diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py
index 39f8267a8..957a820db 100644
--- a/tableauserverclient/__init__.py
+++ b/tableauserverclient/__init__.py
@@ -56,6 +56,7 @@
ExcelRequestOptions,
ImageRequestOptions,
PDFRequestOptions,
+ PPTXRequestOptions,
RequestOptions,
MissingRequiredFieldError,
FailedSignInError,
@@ -107,6 +108,7 @@
"Pager",
"PaginationItem",
"PDFRequestOptions",
+ "PPTXRequestOptions",
"Permission",
"PermissionsRule",
"PersonalAccessTokenAuth",
diff --git a/tableauserverclient/models/connection_item.py b/tableauserverclient/models/connection_item.py
index e68958c3b..6a8244fb1 100644
--- a/tableauserverclient/models/connection_item.py
+++ b/tableauserverclient/models/connection_item.py
@@ -103,11 +103,11 @@ def from_response(cls, resp, ns) -> list["ConnectionItem"]:
all_connection_xml = parsed_response.findall(".//t:connection", namespaces=ns)
for connection_xml in all_connection_xml:
connection_item = cls()
- connection_item._id = connection_xml.get("id", None)
+ connection_item._id = connection_xml.get("id", connection_xml.get("connectionId", None))
connection_item._connection_type = connection_xml.get("type", connection_xml.get("dbClass", None))
connection_item.embed_password = string_to_bool(connection_xml.get("embedPassword", ""))
- connection_item.server_address = connection_xml.get("serverAddress", None)
- connection_item.server_port = connection_xml.get("serverPort", None)
+ connection_item.server_address = connection_xml.get("serverAddress", connection_xml.get("server", None))
+ connection_item.server_port = connection_xml.get("serverPort", connection_xml.get("port", None))
connection_item.username = connection_xml.get("userName", None)
connection_item._query_tagging = (
string_to_bool(s) if (s := connection_xml.get("queryTagging", None)) else None
diff --git a/tableauserverclient/models/datasource_item.py b/tableauserverclient/models/datasource_item.py
index 1b082c157..2005edf7e 100644
--- a/tableauserverclient/models/datasource_item.py
+++ b/tableauserverclient/models/datasource_item.py
@@ -19,6 +19,93 @@
class DatasourceItem:
+ """
+ Represents a Tableau datasource item.
+
+ Parameters
+ ----------
+ project_id : Optional[str]
+ The project ID that the datasource belongs to.
+
+ name : Optional[str]
+ The name of the datasource.
+
+ Attributes
+ ----------
+ ask_data_enablement : Optional[str]
+ Determines if a data source allows use of Ask Data. The value can be
+ TSC.DatasourceItem.AskDataEnablement.Enabled,
+ TSC.DatasourceItem.AskDataEnablement.Disabled, or
+ TSC.DatasourceItem.AskDataEnablement.SiteDefault. If no setting is
+ specified, it will default to SiteDefault. See REST API Publish
+ Datasource for more information about ask_data_enablement.
+
+ connections : list[ConnectionItem]
+ The list of data connections (ConnectionItem) for the specified data
+ source. You must first call the populate_connections method to access
+ this data. See the ConnectionItem class.
+
+ content_url : Optional[str]
+ The name of the data source as it would appear in a URL.
+
+ created_at : Optional[datetime.datetime]
+ The time the data source was created.
+
+ certified : Optional[bool]
+ A Boolean value that indicates whether the data source is certified.
+
+ certification_note : Optional[str]
+ The optional note that describes the certified data source.
+
+ datasource_type : Optional[str]
+ The type of data source, for example, sqlserver or excel-direct.
+
+ description : Optional[str]
+ The description for the data source.
+
+ encrypt_extracts : Optional[bool]
+ A Boolean value to determine if a datasource should be encrypted or not.
+ See Extract and Encryption Methods for more information.
+
+ has_extracts : Optional[bool]
+ A Boolean value that indicates whether the datasource has extracts.
+
+ id : Optional[str]
+ The identifier for the data source. You need this value to query a
+ specific data source or to delete a data source with the get_by_id and
+ delete methods.
+
+ name : Optional[str]
+ The name of the data source. If not specified, the name of the published
+ data source file is used.
+
+ owner_id : Optional[str]
+ The identifier of the owner of the data source.
+
+ project_id : Optional[str]
+ The identifier of the project associated with the data source. You must
+ provide this identifier when you create an instance of a DatasourceItem.
+
+ project_name : Optional[str]
+ The name of the project associated with the data source.
+
+ tags : Optional[set[str]]
+ The tags (list of strings) that have been added to the data source.
+
+ updated_at : Optional[datetime.datetime]
+ The date and time when the data source was last updated.
+
+ use_remote_query_agent : Optional[bool]
+ A Boolean value that indicates whether to allow or disallow your Tableau
+ Cloud site to use Tableau Bridge clients. Bridge allows you to maintain
+ data sources with live connections to supported on-premises data
+ sources. See Configure and Manage the Bridge Client Pool for more
+ information.
+
+ webpage_url : Optional[str]
+ The url of the datasource as displayed in browsers.
+ """
+
class AskDataEnablement:
Enabled = "Enabled"
Disabled = "Disabled"
@@ -33,28 +120,28 @@ def __repr__(self):
)
def __init__(self, project_id: Optional[str] = None, name: Optional[str] = None) -> None:
- self._ask_data_enablement = None
- self._certified = None
- self._certification_note = None
- self._connections = None
+ self._ask_data_enablement: Optional[str] = None
+ self._certified: Optional[bool] = None
+ self._certification_note: Optional[str] = None
+ self._connections: Optional[list[ConnectionItem]] = None
self._content_url: Optional[str] = None
- self._created_at = None
- self._datasource_type = None
- self._description = None
- self._encrypt_extracts = None
- self._has_extracts = None
+ self._created_at: Optional[datetime.datetime] = None
+ self._datasource_type: Optional[str] = None
+ self._description: Optional[str] = None
+ self._encrypt_extracts: Optional[bool] = None
+ self._has_extracts: Optional[bool] = None
self._id: Optional[str] = None
self._initial_tags: set = set()
self._project_name: Optional[str] = None
self._revisions = None
self._size: Optional[int] = None
- self._updated_at = None
- self._use_remote_query_agent = None
- self._webpage_url = None
- self.description = None
- self.name = name
+ self._updated_at: Optional[datetime.datetime] = None
+ self._use_remote_query_agent: Optional[bool] = None
+ self._webpage_url: Optional[str] = None
+ self.description: Optional[str] = None
+ self.name: Optional[str] = name
self.owner_id: Optional[str] = None
- self.project_id = project_id
+ self.project_id: Optional[str] = project_id
self.tags: set[str] = set()
self._permissions = None
@@ -63,16 +150,16 @@ def __init__(self, project_id: Optional[str] = None, name: Optional[str] = None)
return None
@property
- def ask_data_enablement(self) -> Optional[AskDataEnablement]:
+ def ask_data_enablement(self) -> Optional[str]:
return self._ask_data_enablement
@ask_data_enablement.setter
@property_is_enum(AskDataEnablement)
- def ask_data_enablement(self, value: Optional[AskDataEnablement]):
+ def ask_data_enablement(self, value: Optional[str]):
self._ask_data_enablement = value
@property
- def connections(self) -> Optional[list[ConnectionItem]]:
+ def connections(self):
if self._connections is None:
error = "Datasource item must be populated with connections first."
raise UnpopulatedPropertyError(error)
@@ -112,7 +199,7 @@ def certification_note(self, value: Optional[str]):
self._certification_note = value
@property
- def encrypt_extracts(self):
+ def encrypt_extracts(self) -> Optional[bool]:
return self._encrypt_extracts
@encrypt_extracts.setter
@@ -156,7 +243,7 @@ def description(self) -> Optional[str]:
return self._description
@description.setter
- def description(self, value: str):
+ def description(self, value: Optional[str]):
self._description = value
@property
@@ -187,7 +274,7 @@ def revisions(self) -> list[RevisionItem]:
def size(self) -> Optional[int]:
return self._size
- def _set_connections(self, connections):
+ def _set_connections(self, connections) -> None:
self._connections = connections
def _set_permissions(self, permissions):
diff --git a/tableauserverclient/models/job_item.py b/tableauserverclient/models/job_item.py
index 6286275c5..d650eb846 100644
--- a/tableauserverclient/models/job_item.py
+++ b/tableauserverclient/models/job_item.py
@@ -82,6 +82,7 @@ class FinishCode:
Success: int = 0
Failed: int = 1
Cancelled: int = 2
+ Completed: int = 3
def __init__(
self,
diff --git a/tableauserverclient/server/__init__.py b/tableauserverclient/server/__init__.py
index 87cc9460b..55288fdc9 100644
--- a/tableauserverclient/server/__init__.py
+++ b/tableauserverclient/server/__init__.py
@@ -5,6 +5,7 @@
ExcelRequestOptions,
ImageRequestOptions,
PDFRequestOptions,
+ PPTXRequestOptions,
RequestOptions,
)
from tableauserverclient.server.filter import Filter
@@ -52,6 +53,7 @@
"ExcelRequestOptions",
"ImageRequestOptions",
"PDFRequestOptions",
+ "PPTXRequestOptions",
"RequestOptions",
"Filter",
"Sort",
diff --git a/tableauserverclient/server/endpoint/datasources_endpoint.py b/tableauserverclient/server/endpoint/datasources_endpoint.py
index 5a48f3c93..69913a724 100644
--- a/tableauserverclient/server/endpoint/datasources_endpoint.py
+++ b/tableauserverclient/server/endpoint/datasources_endpoint.py
@@ -6,10 +6,11 @@
from contextlib import closing
from pathlib import Path
-from typing import Optional, TYPE_CHECKING, Union
+from typing import Literal, Optional, TYPE_CHECKING, Union, overload
from collections.abc import Iterable, Mapping, Sequence
from tableauserverclient.helpers.headers import fix_filename
+from tableauserverclient.models.dqw_item import DQWItem
from tableauserverclient.server.query import QuerySet
if TYPE_CHECKING:
@@ -71,6 +72,28 @@ def baseurl(self) -> str:
# Get all datasources
@api(version="2.0")
def get(self, req_options: Optional[RequestOptions] = None) -> tuple[list[DatasourceItem], PaginationItem]:
+ """
+ Returns a list of published data sources on the specified site, with
+ optional parameters for specifying the paging of large results. To get
+ a list of data sources embedded in a workbook, use the Query Workbook
+ Connections method.
+
+ Endpoint is paginated, and will return one page per call.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#query_data_sources
+
+ Parameters
+ ----------
+ req_options : Optional[RequestOptions]
+ Optional parameters for the request, such as filters, sorting, page
+ size, and page number.
+
+ Returns
+ -------
+ tuple[list[DatasourceItem], PaginationItem]
+ A tuple containing the list of datasource items and pagination
+ information.
+ """
logger.info("Querying all datasources on site")
url = self.baseurl
server_response = self.get_request(url, req_options)
@@ -81,6 +104,21 @@ def get(self, req_options: Optional[RequestOptions] = None) -> tuple[list[Dataso
# Get 1 datasource by id
@api(version="2.0")
def get_by_id(self, datasource_id: str) -> DatasourceItem:
+ """
+ Returns information about a specific data source.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#query_data_source
+
+ Parameters
+ ----------
+ datasource_id : str
+ The unique ID of the datasource to retrieve.
+
+ Returns
+ -------
+ DatasourceItem
+ An object containing information about the datasource.
+ """
if not datasource_id:
error = "Datasource ID undefined."
raise ValueError(error)
@@ -92,6 +130,20 @@ def get_by_id(self, datasource_id: str) -> DatasourceItem:
# Populate datasource item's connections
@api(version="2.0")
def populate_connections(self, datasource_item: DatasourceItem) -> None:
+ """
+ Retrieve connection information for the specificed datasource item.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#query_data_source_connections
+
+ Parameters
+ ----------
+ datasource_item : DatasourceItem
+ The datasource item to retrieve connections for.
+
+ Returns
+ -------
+ None
+ """
if not datasource_item.id:
error = "Datasource item missing ID. Datasource must be retrieved from server first."
raise MissingRequiredFieldError(error)
@@ -116,6 +168,22 @@ def _get_datasource_connections(
# Delete 1 datasource by id
@api(version="2.0")
def delete(self, datasource_id: str) -> None:
+ """
+ Deletes the specified data source from a site. When a data source is
+ deleted, its associated data connection is also deleted. Workbooks that
+ use the data source are not deleted, but they will no longer work
+ properly.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#delete_data_source
+
+ Parameters
+ ----------
+ datasource_id : str
+
+ Returns
+ -------
+ None
+ """
if not datasource_id:
error = "Datasource ID undefined."
raise ValueError(error)
@@ -133,6 +201,29 @@ def download(
filepath: Optional[PathOrFileW] = None,
include_extract: bool = True,
) -> PathOrFileW:
+ """
+ Downloads the specified data source from a site. The data source is
+ downloaded as a .tdsx file.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#download_data_source
+
+ Parameters
+ ----------
+ datasource_id : str
+ The unique ID of the datasource to download.
+
+ filepath : Optional[PathOrFileW]
+ The file path to save the downloaded datasource to. If not
+ specified, the file will be saved to the current working directory.
+
+ include_extract : bool, default True
+ If True, the extract is included in the download. If False, the
+ extract is not included.
+
+ Returns
+ -------
+ filepath : PathOrFileW
+ """
return self.download_revision(
datasource_id,
None,
@@ -143,6 +234,28 @@ def download(
# Update datasource
@api(version="2.0")
def update(self, datasource_item: DatasourceItem) -> DatasourceItem:
+ """
+ Updates the owner, project or certification status of the specified
+ data source.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#update_data_source
+
+ Parameters
+ ----------
+ datasource_item : DatasourceItem
+ The datasource item to update.
+
+ Returns
+ -------
+ DatasourceItem
+ An object containing information about the updated datasource.
+
+ Raises
+ ------
+ MissingRequiredFieldError
+ If the datasource item is missing an ID.
+ """
+
if not datasource_item.id:
error = "Datasource item missing ID. Datasource must be retrieved from server first."
raise MissingRequiredFieldError(error)
@@ -171,6 +284,26 @@ def update(self, datasource_item: DatasourceItem) -> DatasourceItem:
def update_connection(
self, datasource_item: DatasourceItem, connection_item: ConnectionItem
) -> Optional[ConnectionItem]:
+ """
+ Updates the server address, port, username, or password for the
+ specified data source connection.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#update_data_source_connection
+
+ Parameters
+ ----------
+ datasource_item : DatasourceItem
+ The datasource item to update.
+
+ connection_item : ConnectionItem
+ The connection item to update.
+
+ Returns
+ -------
+ Optional[ConnectionItem]
+ An object containing information about the updated connection.
+ """
+
url = f"{self.baseurl}/{datasource_item.id}/connections/{connection_item.id}"
update_req = RequestFactory.Connection.update_req(connection_item)
@@ -188,16 +321,51 @@ def update_connection(
@api(version="2.8")
def refresh(self, datasource_item: DatasourceItem, incremental: bool = False) -> JobItem:
+ """
+ Refreshes the extract of an existing workbook.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_jobs_tasks_and_schedules.htm#run_extract_refresh_task
+
+ Parameters
+ ----------
+ workbook_item : WorkbookItem | str
+ The workbook item or workbook ID.
+ incremental: bool
+ Whether to do a full refresh or incremental refresh of the extract data
+
+ Returns
+ -------
+ JobItem
+ The job item.
+ """
id_ = getattr(datasource_item, "id", datasource_item)
url = f"{self.baseurl}/{id_}/refresh"
- # refresh_req = RequestFactory.Task.refresh_req(incremental)
- refresh_req = RequestFactory.Empty.empty_req()
+ refresh_req = RequestFactory.Task.refresh_req(incremental, self.parent_srv)
server_response = self.post_request(url, refresh_req)
new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
return new_job
@api(version="3.5")
def create_extract(self, datasource_item: DatasourceItem, encrypt: bool = False) -> JobItem:
+ """
+ Create an extract for a data source in a site. Optionally, encrypt the
+ extract if the site and workbooks using it are configured to allow it.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_extract_and_encryption.htm#create_extract_for_datasource
+
+ Parameters
+ ----------
+ datasource_item : DatasourceItem | str
+ The datasource item or datasource ID.
+
+ encrypt : bool, default False
+ Whether to encrypt the extract.
+
+ Returns
+ -------
+ JobItem
+ The job item.
+ """
id_ = getattr(datasource_item, "id", datasource_item)
url = f"{self.baseurl}/{id_}/createExtract?encrypt={encrypt}"
empty_req = RequestFactory.Empty.empty_req()
@@ -207,11 +375,49 @@ def create_extract(self, datasource_item: DatasourceItem, encrypt: bool = False)
@api(version="3.5")
def delete_extract(self, datasource_item: DatasourceItem) -> None:
+ """
+ Delete the extract of a data source in a site.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_extract_and_encryption.htm#delete_extract_from_datasource
+
+ Parameters
+ ----------
+ datasource_item : DatasourceItem | str
+ The datasource item or datasource ID.
+
+ Returns
+ -------
+ None
+ """
id_ = getattr(datasource_item, "id", datasource_item)
url = f"{self.baseurl}/{id_}/deleteExtract"
empty_req = RequestFactory.Empty.empty_req()
self.post_request(url, empty_req)
+ @overload
+ def publish(
+ self,
+ datasource_item: DatasourceItem,
+ file: PathOrFileR,
+ mode: str,
+ connection_credentials: Optional[ConnectionCredentials] = None,
+ connections: Optional[Sequence[ConnectionItem]] = None,
+ as_job: Literal[False] = False,
+ ) -> DatasourceItem:
+ pass
+
+ @overload
+ def publish(
+ self,
+ datasource_item: DatasourceItem,
+ file: PathOrFileR,
+ mode: str,
+ connection_credentials: Optional[ConnectionCredentials] = None,
+ connections: Optional[Sequence[ConnectionItem]] = None,
+ as_job: Literal[True] = True,
+ ) -> JobItem:
+ pass
+
# Publish datasource
@api(version="2.0")
@parameter_added_in(connections="2.8")
@@ -225,6 +431,50 @@ def publish(
connections: Optional[Sequence[ConnectionItem]] = None,
as_job: bool = False,
) -> Union[DatasourceItem, JobItem]:
+ """
+ Publishes a data source to a server, or appends data to an existing
+ data source.
+
+ This method checks the size of the data source and automatically
+ determines whether the publish the data source in multiple parts or in
+ one operation.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#publish_data_source
+
+ Parameters
+ ----------
+ datasource_item : DatasourceItem
+ The datasource item to publish. The fields for name and project_id
+ are required.
+
+ file : PathOrFileR
+ The file path or file object to publish.
+
+ mode : str
+ Specifies whether you are publishing a new datasource (CreateNew),
+ overwriting an existing datasource (Overwrite), or add to an
+ existing datasource (Append). You can also use the publish mode
+ attributes, for example: TSC.Server.PublishMode.Overwrite.
+
+ connection_credentials : Optional[ConnectionCredentials]
+ The connection credentials to use when publishing the datasource.
+ Mutually exclusive with the connections parameter.
+
+ connections : Optional[Sequence[ConnectionItem]]
+ The connections to use when publishing the datasource. Mutually
+ exclusive with the connection_credentials parameter.
+
+ as_job : bool, default False
+ If True, the publish operation is asynchronous and returns a job
+ item. If False, the publish operation is synchronous and returns a
+ datasource item.
+
+ Returns
+ -------
+ Union[DatasourceItem, JobItem]
+ The datasource item or job item.
+
+ """
if isinstance(file, (os.PathLike, str)):
if not os.path.isfile(file):
error = "File path does not lead to an existing file."
@@ -329,6 +579,51 @@ def update_hyper_data(
actions: Sequence[Mapping],
payload: Optional[FilePath] = None,
) -> JobItem:
+ """
+ Incrementally updates data (insert, update, upsert, replace and delete)
+ in a published data source from a live-to-Hyper connection, where the
+ data source has multiple connections.
+
+ A live-to-Hyper connection has a Hyper or Parquet formatted
+ file/database as the origin of its data.
+
+ For all connections to Parquet files, and for any data sources with a
+ single connection generally, you can use the Update Data in Hyper Data
+ Source method without specifying the connection-id.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#update_data_in_hyper_connection
+
+ Parameters
+ ----------
+ datasource_or_connection_item : Union[DatasourceItem, ConnectionItem, str]
+ The datasource item, connection item, or datasource ID. Either a
+ DataSourceItem or a ConnectionItem. If the datasource only contains
+ a single connection, the DataSourceItem is sufficient to identify
+ which data should be updated. Otherwise, for datasources with
+ multiple connections, a ConnectionItem must be provided.
+
+ request_id : str
+ User supplied arbitrary string to identify the request. A request
+ identified with the same key will only be executed once, even if
+ additional requests using the key are made, for instance, due to
+ retries when facing network issues.
+
+ actions : Sequence[Mapping]
+ A list of actions (insert, update, delete, ...) specifying how to
+ modify the data within the published datasource. For more
+ information on the actions, see: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_how_to_update_data_to_hyper.htm#action-batch-descriptions
+
+ payload : Optional[FilePath]
+ A Hyper file containing tuples to be inserted/deleted/updated or
+ other payload data used by the actions. Hyper files can be created
+ using the Tableau Hyper API or pantab.
+
+ Returns
+ -------
+ JobItem
+ The job running on the server.
+
+ """
if isinstance(datasource_or_connection_item, DatasourceItem):
datasource_id = datasource_or_connection_item.id
url = f"{self.baseurl}/{datasource_id}/data"
@@ -357,35 +652,179 @@ def update_hyper_data(
@api(version="2.0")
def populate_permissions(self, item: DatasourceItem) -> None:
+ """
+ Populates the permissions on the specified datasource item.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_permissions.htm#query_data_source_permissions
+
+ Parameters
+ ----------
+ item : DatasourceItem
+ The datasource item to populate permissions for.
+
+ Returns
+ -------
+ None
+ """
self._permissions.populate(item)
@api(version="2.0")
def update_permissions(self, item: DatasourceItem, permission_item: list["PermissionsRule"]) -> None:
+ """
+ Updates the permissions on the specified datasource item. This method
+ overwrites all existing permissions. Any permissions not included in
+ the list will be removed.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_permissions.htm#replace_permissions_for_content
+
+ Parameters
+ ----------
+ item : DatasourceItem
+ The datasource item to update permissions for.
+
+ permission_item : list[PermissionsRule]
+ The permissions to apply to the datasource item.
+
+ Returns
+ -------
+ None
+ """
self._permissions.update(item, permission_item)
@api(version="2.0")
def delete_permission(self, item: DatasourceItem, capability_item: "PermissionsRule") -> None:
+ """
+ Deletes a single permission rule from the specified datasource item.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_permissions.htm#delete_data_source_permissionDatasourceItem
+
+ Parameters
+ ----------
+ item : DatasourceItem
+ The datasource item to delete permissions from.
+
+ capability_item : PermissionsRule
+ The permission rule to delete.
+
+ Returns
+ -------
+ None
+ """
self._permissions.delete(item, capability_item)
@api(version="3.5")
- def populate_dqw(self, item):
+ def populate_dqw(self, item) -> None:
+ """
+ Get information about the data quality warning for the database, table,
+ column, published data source, flow, virtual connection, or virtual
+ connection table.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_metadata.htm#query_dqws
+
+ Parameters
+ ----------
+ item : DatasourceItem
+ The datasource item to populate data quality warnings for.
+
+ Returns
+ -------
+ None
+ """
self._data_quality_warnings.populate(item)
@api(version="3.5")
def update_dqw(self, item, warning):
+ """
+ Update the warning type, status, and message of a data quality warning.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_metadata.htm#update_dqw
+
+ Parameters
+ ----------
+ item : DatasourceItem
+ The datasource item to update data quality warnings for.
+
+ warning : DQWItem
+ The data quality warning to update.
+
+ Returns
+ -------
+ DQWItem
+ The updated data quality warning.
+ """
return self._data_quality_warnings.update(item, warning)
@api(version="3.5")
def add_dqw(self, item, warning):
+ """
+ Add a data quality warning to a datasource.
+
+ The Add Data Quality Warning method adds a data quality warning to an
+ asset. (An automatically-generated monitoring warning does not count
+ towards this limit.) In Tableau Cloud February 2024 and Tableau Server
+ 2024.2 and earlier, adding a data quality warning to an asset that
+ already has one causes an error.
+
+ This method is available if your Tableau Cloud site or Tableau Server is licensed with Data Management.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_metadata.htm#add_dqw
+
+ Parameters
+ ----------
+ item: DatasourceItem
+ The datasource item to add data quality warnings to.
+
+ warning: DQWItem
+ The data quality warning to add.
+
+ Returns
+ -------
+ DQWItem
+ The added data quality warning.
+
+ """
return self._data_quality_warnings.add(item, warning)
@api(version="3.5")
def delete_dqw(self, item):
+ """
+ Delete a data quality warnings from an asset.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_metadata.htm#delete_dqws
+
+ Parameters
+ ----------
+ item: DatasourceItem
+ The datasource item to delete data quality warnings from.
+
+ Returns
+ -------
+ None
+ """
self._data_quality_warnings.clear(item)
# Populate datasource item's revisions
@api(version="2.3")
def populate_revisions(self, datasource_item: DatasourceItem) -> None:
+ """
+ Retrieve revision information for the specified datasource item.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#get_data_source_revisions
+
+ Parameters
+ ----------
+ datasource_item : DatasourceItem
+ The datasource item to retrieve revisions for.
+
+ Returns
+ -------
+ None
+
+ Raises
+ ------
+ MissingRequiredFieldError
+ If the datasource item is missing an ID.
+ """
if not datasource_item.id:
error = "Datasource item missing ID. Datasource must be retrieved from server first."
raise MissingRequiredFieldError(error)
@@ -413,6 +852,35 @@ def download_revision(
filepath: Optional[PathOrFileW] = None,
include_extract: bool = True,
) -> PathOrFileW:
+ """
+ Downloads a specific version of a data source prior to the current one
+ in .tdsx format. To download the current version of a data source set
+ the revision number to None.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#download_data_source_revision
+
+ Parameters
+ ----------
+ datasource_id : str
+ The unique ID of the datasource to download.
+
+ revision_number : Optional[str]
+ The revision number of the data source to download. To determine
+ what versions are available, call the `populate_revisions` method.
+ Pass None to download the current version.
+
+ filepath : Optional[PathOrFileW]
+ The file path to save the downloaded datasource to. If not
+ specified, the file will be saved to the current working directory.
+
+ include_extract : bool, default True
+ If True, the extract is included in the download. If False, the
+ extract is not included.
+
+ Returns
+ -------
+ filepath : PathOrFileW
+ """
if not datasource_id:
error = "Datasource ID undefined."
raise ValueError(error)
@@ -446,6 +914,28 @@ def download_revision(
@api(version="2.3")
def delete_revision(self, datasource_id: str, revision_number: str) -> None:
+ """
+ Removes a specific version of a data source from the specified site.
+
+ The content is removed, and the specified revision can no longer be
+ downloaded using Download Data Source Revision. If you call Get Data
+ Source Revisions, the revision that's been removed is listed with the
+ attribute is_deleted=True.
+
+ REST API:https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#remove_data_source_revision
+
+ Parameters
+ ----------
+ datasource_id : str
+ The unique ID of the datasource to delete.
+
+ revision_number : str
+ The revision number of the data source to delete.
+
+ Returns
+ -------
+ None
+ """
if datasource_id is None or revision_number is None:
raise ValueError
url = "/".join([self.baseurl, datasource_id, "revisions", revision_number])
@@ -458,18 +948,83 @@ def delete_revision(self, datasource_id: str, revision_number: str) -> None:
def schedule_extract_refresh(
self, schedule_id: str, item: DatasourceItem
) -> list["AddResponse"]: # actually should return a task
+ """
+ Adds a task to refresh a data source to an existing server schedule on
+ Tableau Server.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_jobs_tasks_and_schedules.htm#add_data_source_to_schedule
+
+ Parameters
+ ----------
+ schedule_id : str
+ The unique ID of the schedule to add the task to.
+
+ item : DatasourceItem
+ The datasource item to add to the schedule.
+
+ Returns
+ -------
+ list[AddResponse]
+ """
return self.parent_srv.schedules.add_to_schedule(schedule_id, datasource=item)
@api(version="1.0")
def add_tags(self, item: Union[DatasourceItem, str], tags: Union[Iterable[str], str]) -> set[str]:
+ """
+ Adds one or more tags to the specified datasource item.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#add_tags_to_data_source
+
+ Parameters
+ ----------
+ item : Union[DatasourceItem, str]
+ The datasource item or ID to add tags to.
+
+ tags : Union[Iterable[str], str]
+ The tag or tags to add to the datasource item.
+
+ Returns
+ -------
+ set[str]
+ The updated set of tags on the datasource item.
+ """
return super().add_tags(item, tags)
@api(version="1.0")
def delete_tags(self, item: Union[DatasourceItem, str], tags: Union[Iterable[str], str]) -> None:
+ """
+ Deletes one or more tags from the specified datasource item.
+
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#delete_tag_from_data_source
+
+ Parameters
+ ----------
+ item : Union[DatasourceItem, str]
+ The datasource item or ID to delete tags from.
+
+ tags : Union[Iterable[str], str]
+ The tag or tags to delete from the datasource item.
+
+ Returns
+ -------
+ None
+ """
return super().delete_tags(item, tags)
@api(version="1.0")
def update_tags(self, item: DatasourceItem) -> None:
+ """
+ Updates the tags on the server to match the specified datasource item.
+
+ Parameters
+ ----------
+ item : DatasourceItem
+ The datasource item to update tags for.
+
+ Returns
+ -------
+ None
+ """
return super().update_tags(item)
def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[DatasourceItem]:
diff --git a/tableauserverclient/server/endpoint/exceptions.py b/tableauserverclient/server/endpoint/exceptions.py
index 77332da3e..ee931c910 100644
--- a/tableauserverclient/server/endpoint/exceptions.py
+++ b/tableauserverclient/server/endpoint/exceptions.py
@@ -113,3 +113,7 @@ def __str__(self):
class FlowRunCancelledException(FlowRunFailedException):
pass
+
+
+class UnsupportedAttributeError(TableauError):
+ pass
diff --git a/tableauserverclient/server/endpoint/fileuploads_endpoint.py b/tableauserverclient/server/endpoint/fileuploads_endpoint.py
index 1ae10e72d..c1749af40 100644
--- a/tableauserverclient/server/endpoint/fileuploads_endpoint.py
+++ b/tableauserverclient/server/endpoint/fileuploads_endpoint.py
@@ -56,6 +56,6 @@ def upload(self, file):
request, content_type = RequestFactory.Fileupload.chunk_req(chunk)
logger.debug(f"{datetime.timestamp()} created chunk request")
fileupload_item = self.append(upload_id, request, content_type)
- logger.info(f"\t{datetime.timestamp()} Published {(fileupload_item.file_size / BYTES_PER_MB)}MB")
+ logger.info(f"\t{datetime.timestamp()} Published {fileupload_item.file_size}MB")
logger.info(f"File upload finished (ID: {upload_id})")
return upload_id
diff --git a/tableauserverclient/server/endpoint/groupsets_endpoint.py b/tableauserverclient/server/endpoint/groupsets_endpoint.py
index c7f5ed0e5..8c0ef64f3 100644
--- a/tableauserverclient/server/endpoint/groupsets_endpoint.py
+++ b/tableauserverclient/server/endpoint/groupsets_endpoint.py
@@ -25,14 +25,14 @@ def baseurl(self) -> str:
@api(version="3.22")
def get(
self,
- request_options: Optional[RequestOptions] = None,
+ req_options: Optional[RequestOptions] = None,
result_level: Optional[Literal["members", "local"]] = None,
) -> tuple[list[GroupSetItem], PaginationItem]:
logger.info("Querying all group sets on site")
url = self.baseurl
if result_level:
url += f"?resultlevel={result_level}"
- server_response = self.get_request(url, request_options)
+ server_response = self.get_request(url, req_options)
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
all_group_set_items = GroupSetItem.from_response(server_response.content, self.parent_srv.namespace)
return all_group_set_items, pagination_item
diff --git a/tableauserverclient/server/endpoint/jobs_endpoint.py b/tableauserverclient/server/endpoint/jobs_endpoint.py
index 027a7ca12..48e91bd74 100644
--- a/tableauserverclient/server/endpoint/jobs_endpoint.py
+++ b/tableauserverclient/server/endpoint/jobs_endpoint.py
@@ -188,7 +188,7 @@ def wait_for_job(self, job_id: Union[str, JobItem], *, timeout: Optional[float]
logger.info(f"Job {job_id} Completed: Finish Code: {job.finish_code} - Notes:{job.notes}")
- if job.finish_code == JobItem.FinishCode.Success:
+ if job.finish_code in [JobItem.FinishCode.Success, JobItem.FinishCode.Completed]:
return job
elif job.finish_code == JobItem.FinishCode.Failed:
raise JobFailedException(job)
diff --git a/tableauserverclient/server/endpoint/views_endpoint.py b/tableauserverclient/server/endpoint/views_endpoint.py
index 12b386876..9d1c8b00f 100644
--- a/tableauserverclient/server/endpoint/views_endpoint.py
+++ b/tableauserverclient/server/endpoint/views_endpoint.py
@@ -3,7 +3,7 @@
from tableauserverclient.models.permissions_item import PermissionsRule
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
-from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
+from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError, UnsupportedAttributeError
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
from tableauserverclient.server.query import QuerySet
@@ -171,6 +171,10 @@ def populate_image(self, view_item: ViewItem, req_options: Optional["ImageReques
def image_fetcher():
return self._get_view_image(view_item, req_options)
+ if not self.parent_srv.check_at_least_version("3.23") and req_options is not None:
+ if req_options.viz_height or req_options.viz_width:
+ raise UnsupportedAttributeError("viz_height and viz_width are only supported in 3.23+")
+
view_item._set_image(image_fetcher)
logger.info(f"Populated image for view (ID: {view_item.id})")
diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py
index 4fdcf075b..bf4088b9f 100644
--- a/tableauserverclient/server/endpoint/workbooks_endpoint.py
+++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py
@@ -11,7 +11,11 @@
from tableauserverclient.server.query import QuerySet
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api, parameter_added_in
-from tableauserverclient.server.endpoint.exceptions import InternalServerError, MissingRequiredFieldError
+from tableauserverclient.server.endpoint.exceptions import (
+ InternalServerError,
+ MissingRequiredFieldError,
+ UnsupportedAttributeError,
+)
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
@@ -34,7 +38,7 @@
if TYPE_CHECKING:
from tableauserverclient.server import Server
- from tableauserverclient.server.request_options import RequestOptions
+ from tableauserverclient.server.request_options import RequestOptions, PDFRequestOptions, PPTXRequestOptions
from tableauserverclient.models import DatasourceItem
from tableauserverclient.server.endpoint.schedules_endpoint import AddResponse
@@ -136,7 +140,7 @@ def refresh(self, workbook_item: Union[WorkbookItem, str], incremental: bool = F
"""
id_ = getattr(workbook_item, "id", workbook_item)
url = f"{self.baseurl}/{id_}/refresh"
- refresh_req = RequestFactory.Task.refresh_req(incremental)
+ refresh_req = RequestFactory.Task.refresh_req(incremental, self.parent_srv)
server_response = self.post_request(url, refresh_req)
new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
return new_job
@@ -472,11 +476,12 @@ def _get_workbook_connections(
connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
return connections
- # Get the pdf of the entire workbook if its tabs are enabled, pdf of the default view if its tabs are disabled
@api(version="3.4")
- def populate_pdf(self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"] = None) -> None:
+ def populate_pdf(self, workbook_item: WorkbookItem, req_options: Optional["PDFRequestOptions"] = None) -> None:
"""
- Populates the PDF for the specified workbook item.
+ Populates the PDF for the specified workbook item. Get the pdf of the
+ entire workbook if its tabs are enabled, pdf of the default view if its
+ tabs are disabled.
This method populates a PDF with image(s) of the workbook view(s) you
specify.
@@ -488,7 +493,7 @@ def populate_pdf(self, workbook_item: WorkbookItem, req_options: Optional["Reque
workbook_item : WorkbookItem
The workbook item to populate the PDF for.
- req_options : RequestOptions, optional
+ req_options : PDFRequestOptions, optional
(Optional) You can pass in request options to specify the page type
and orientation of the PDF content, as well as the maximum age of
the PDF rendered on the server. See PDFRequestOptions class for more
@@ -510,17 +515,26 @@ def populate_pdf(self, workbook_item: WorkbookItem, req_options: Optional["Reque
def pdf_fetcher() -> bytes:
return self._get_wb_pdf(workbook_item, req_options)
+ if not self.parent_srv.check_at_least_version("3.23") and req_options is not None:
+ if req_options.view_filters or req_options.view_parameters:
+ raise UnsupportedAttributeError("view_filters and view_parameters are only supported in 3.23+")
+
+ if req_options.viz_height or req_options.viz_width:
+ raise UnsupportedAttributeError("viz_height and viz_width are only supported in 3.23+")
+
workbook_item._set_pdf(pdf_fetcher)
logger.info(f"Populated pdf for workbook (ID: {workbook_item.id})")
- def _get_wb_pdf(self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"]) -> bytes:
+ def _get_wb_pdf(self, workbook_item: WorkbookItem, req_options: Optional["PDFRequestOptions"]) -> bytes:
url = f"{self.baseurl}/{workbook_item.id}/pdf"
server_response = self.get_request(url, req_options)
pdf = server_response.content
return pdf
@api(version="3.8")
- def populate_powerpoint(self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"] = None) -> None:
+ def populate_powerpoint(
+ self, workbook_item: WorkbookItem, req_options: Optional["PPTXRequestOptions"] = None
+ ) -> None:
"""
Populates the PowerPoint for the specified workbook item.
@@ -561,7 +575,7 @@ def pptx_fetcher() -> bytes:
workbook_item._set_powerpoint(pptx_fetcher)
logger.info(f"Populated powerpoint for workbook (ID: {workbook_item.id})")
- def _get_wb_pptx(self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"]) -> bytes:
+ def _get_wb_pptx(self, workbook_item: WorkbookItem, req_options: Optional["PPTXRequestOptions"]) -> bytes:
url = f"{self.baseurl}/{workbook_item.id}/powerpoint"
server_response = self.get_request(url, req_options)
pptx = server_response.content
diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py
index 79ac6e4ca..575423612 100644
--- a/tableauserverclient/server/request_factory.py
+++ b/tableauserverclient/server/request_factory.py
@@ -1118,11 +1118,17 @@ def run_req(self, xml_request: ET.Element, task_item: Any) -> None:
pass
@_tsrequest_wrapped
- def refresh_req(self, xml_request: ET.Element, incremental: bool = False) -> bytes:
- task_element = ET.SubElement(xml_request, "extractRefresh")
- if incremental:
- task_element.attrib["incremental"] = "true"
- return ET.tostring(xml_request)
+ def refresh_req(
+ self, xml_request: ET.Element, incremental: bool = False, parent_srv: Optional["Server"] = None
+ ) -> Optional[bytes]:
+ if parent_srv is not None and parent_srv.check_at_least_version("3.25"):
+ task_element = ET.SubElement(xml_request, "extractRefresh")
+ if incremental:
+ task_element.attrib["incremental"] = "true"
+ return ET.tostring(xml_request)
+ elif incremental:
+ raise ValueError("Incremental refresh is only supported in 3.25+")
+ return None
@_tsrequest_wrapped
def create_extract_req(self, xml_request: ET.Element, extract_item: "TaskItem") -> bytes:
diff --git a/tableauserverclient/server/request_options.py b/tableauserverclient/server/request_options.py
index c37c0ce42..504f7f3ca 100644
--- a/tableauserverclient/server/request_options.py
+++ b/tableauserverclient/server/request_options.py
@@ -385,6 +385,8 @@ class PDFRequestOptions(_ImagePDFCommonExportOptions):
Options that can be used when exporting a view to PDF. Set the maxage to control the age of the data exported.
Filters to the underlying data can be applied using the `vf` and `parameter` methods.
+ vf and parameter filters are only supported in API version 3.23 and later.
+
Parameters
----------
page_type: str, optional
@@ -438,3 +440,35 @@ def get_query_params(self) -> dict:
params["orientation"] = self.orientation
return params
+
+
+class PPTXRequestOptions(RequestOptionsBase):
+ """
+ Options that can be used when exporting a view to PPTX. Set the maxage to control the age of the data exported.
+
+ Parameters
+ ----------
+ maxage: int, optional
+ The maximum age of the data to export. Shortest possible duration is 1
+ minute. No upper limit. Default is -1, which means no limit.
+ """
+
+ def __init__(self, maxage=-1):
+ super().__init__()
+ self.max_age = maxage
+
+ @property
+ def max_age(self) -> int:
+ return self._max_age
+
+ @max_age.setter
+ @property_is_int(range=(0, 240), allowed=[-1])
+ def max_age(self, value):
+ self._max_age = value
+
+ def get_query_params(self):
+ params = {}
+ if self.max_age != -1:
+ params["maxAge"] = self.max_age
+
+ return params
diff --git a/test/assets/job_get_by_id_completed.xml b/test/assets/job_get_by_id_completed.xml
new file mode 100644
index 000000000..95ca29b49
--- /dev/null
+++ b/test/assets/job_get_by_id_completed.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ Job detail notes
+
+
+ More detail
+
+
+
\ No newline at end of file
diff --git a/test/assets/virtual_connection_populate_connections2.xml b/test/assets/virtual_connection_populate_connections2.xml
new file mode 100644
index 000000000..f0ad2646d
--- /dev/null
+++ b/test/assets/virtual_connection_populate_connections2.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/test/request_factory/test_task_requests.py b/test/request_factory/test_task_requests.py
new file mode 100644
index 000000000..0258b8a93
--- /dev/null
+++ b/test/request_factory/test_task_requests.py
@@ -0,0 +1,48 @@
+import unittest
+import xml.etree.ElementTree as ET
+from unittest.mock import Mock
+from tableauserverclient.server.request_factory import TaskRequest
+
+
+class TestTaskRequest(unittest.TestCase):
+
+ def setUp(self):
+ self.task_request = TaskRequest()
+ self.xml_request = ET.Element("tsRequest")
+
+ def test_refresh_req_default(self):
+ result = self.task_request.refresh_req()
+ self.assertEqual(result, ET.tostring(self.xml_request))
+
+ def test_refresh_req_incremental(self):
+ with self.assertRaises(ValueError):
+ self.task_request.refresh_req(incremental=True)
+
+ def test_refresh_req_with_parent_srv_version_3_25(self):
+ parent_srv = Mock()
+ parent_srv.check_at_least_version.return_value = True
+ result = self.task_request.refresh_req(incremental=True, parent_srv=parent_srv)
+ expected_xml = ET.Element("tsRequest")
+ task_element = ET.SubElement(expected_xml, "extractRefresh")
+ task_element.attrib["incremental"] = "true"
+ self.assertEqual(result, ET.tostring(expected_xml))
+
+ def test_refresh_req_with_parent_srv_version_3_25_non_incremental(self):
+ parent_srv = Mock()
+ parent_srv.check_at_least_version.return_value = True
+ result = self.task_request.refresh_req(incremental=False, parent_srv=parent_srv)
+ expected_xml = ET.Element("tsRequest")
+ ET.SubElement(expected_xml, "extractRefresh")
+ self.assertEqual(result, ET.tostring(expected_xml))
+
+ def test_refresh_req_with_parent_srv_version_below_3_25(self):
+ parent_srv = Mock()
+ parent_srv.check_at_least_version.return_value = False
+ with self.assertRaises(ValueError):
+ self.task_request.refresh_req(incremental=True, parent_srv=parent_srv)
+
+ def test_refresh_req_with_parent_srv_version_below_3_25_non_incremental(self):
+ parent_srv = Mock()
+ parent_srv.check_at_least_version.return_value = False
+ result = self.task_request.refresh_req(incremental=False, parent_srv=parent_srv)
+ self.assertEqual(result, ET.tostring(self.xml_request))
diff --git a/test/test_job.py b/test/test_job.py
index 20b238764..b3d7007aa 100644
--- a/test/test_job.py
+++ b/test/test_job.py
@@ -11,6 +11,7 @@
GET_XML = "job_get.xml"
GET_BY_ID_XML = "job_get_by_id.xml"
+GET_BY_ID_COMPLETED_XML = "job_get_by_id_completed.xml"
GET_BY_ID_FAILED_XML = "job_get_by_id_failed.xml"
GET_BY_ID_CANCELLED_XML = "job_get_by_id_cancelled.xml"
GET_BY_ID_INPROGRESS_XML = "job_get_by_id_inprogress.xml"
@@ -87,6 +88,17 @@ def test_wait_for_job_finished(self) -> None:
self.assertEqual(job_id, job.id)
self.assertListEqual(job.notes, ["Job detail notes"])
+ def test_wait_for_job_completed(self) -> None:
+ # Waiting for a bridge (cloud) job completion
+ response_xml = read_xml_asset(GET_BY_ID_COMPLETED_XML)
+ job_id = "2eef4225-aa0c-41c4-8662-a76d89ed7336"
+ with mocked_time(), requests_mock.mock() as m:
+ m.get(f"{self.baseurl}/{job_id}", text=response_xml)
+ job = self.server.jobs.wait_for_job(job_id)
+
+ self.assertEqual(job_id, job.id)
+ self.assertListEqual(job.notes, ["Job detail notes"])
+
def test_wait_for_job_failed(self) -> None:
# Waiting for a failed job raises an exception
response_xml = read_xml_asset(GET_BY_ID_FAILED_XML)
diff --git a/test/test_view.py b/test/test_view.py
index a89a6d235..3fdaf60e6 100644
--- a/test/test_view.py
+++ b/test/test_view.py
@@ -6,6 +6,7 @@
import tableauserverclient as TSC
from tableauserverclient import UserItem, GroupItem, PermissionsRule
from tableauserverclient.datetime_helpers import format_datetime
+from tableauserverclient.server.endpoint.exceptions import UnsupportedAttributeError
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets")
@@ -177,6 +178,43 @@ def test_populate_image(self) -> None:
self.server.views.populate_image(single_view)
self.assertEqual(response, single_view.image)
+ def test_populate_image_unsupported(self) -> None:
+ self.server.version = "3.8"
+ with open(POPULATE_PREVIEW_IMAGE, "rb") as f:
+ response = f.read()
+ with requests_mock.mock() as m:
+ m.get(
+ self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?vizWidth=1920&vizHeight=1080",
+ content=response,
+ )
+ single_view = TSC.ViewItem()
+ single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5"
+
+ req_option = TSC.ImageRequestOptions(viz_width=1920, viz_height=1080)
+
+ with self.assertRaises(UnsupportedAttributeError):
+ self.server.views.populate_image(single_view, req_option)
+
+ def test_populate_image_viz_dimensions(self) -> None:
+ self.server.version = "3.23"
+ self.baseurl = self.server.views.baseurl
+ with open(POPULATE_PREVIEW_IMAGE, "rb") as f:
+ response = f.read()
+ with requests_mock.mock() as m:
+ m.get(
+ self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?vizWidth=1920&vizHeight=1080",
+ content=response,
+ )
+ single_view = TSC.ViewItem()
+ single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5"
+
+ req_option = TSC.ImageRequestOptions(viz_width=1920, viz_height=1080)
+
+ self.server.views.populate_image(single_view, req_option)
+ self.assertEqual(response, single_view.image)
+
+ history = m.request_history
+
def test_populate_image_with_options(self) -> None:
with open(POPULATE_PREVIEW_IMAGE, "rb") as f:
response = f.read()
diff --git a/test/test_virtual_connection.py b/test/test_virtual_connection.py
index 975033d2d..5d9a2d1bc 100644
--- a/test/test_virtual_connection.py
+++ b/test/test_virtual_connection.py
@@ -2,6 +2,7 @@
from pathlib import Path
import unittest
+import pytest
import requests_mock
import tableauserverclient as TSC
@@ -12,6 +13,7 @@
VIRTUAL_CONNECTION_GET_XML = ASSET_DIR / "virtual_connections_get.xml"
VIRTUAL_CONNECTION_POPULATE_CONNECTIONS = ASSET_DIR / "virtual_connection_populate_connections.xml"
+VIRTUAL_CONNECTION_POPULATE_CONNECTIONS2 = ASSET_DIR / "virtual_connection_populate_connections2.xml"
VC_DB_CONN_UPDATE = ASSET_DIR / "virtual_connection_database_connection_update.xml"
VIRTUAL_CONNECTION_DOWNLOAD = ASSET_DIR / "virtual_connections_download.xml"
VIRTUAL_CONNECTION_UPDATE = ASSET_DIR / "virtual_connections_update.xml"
@@ -54,23 +56,27 @@ def test_virtual_connection_get(self):
assert items[0].name == "vconn"
def test_virtual_connection_populate_connections(self):
- vconn = VirtualConnectionItem("vconn")
- vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79"
- with requests_mock.mock() as m:
- m.get(f"{self.baseurl}/{vconn.id}/connections", text=VIRTUAL_CONNECTION_POPULATE_CONNECTIONS.read_text())
- vc_out = self.server.virtual_connections.populate_connections(vconn)
- connection_list = list(vconn.connections)
-
- assert vc_out is vconn
- assert vc_out._connections is not None
-
- assert len(connection_list) == 1
- connection = connection_list[0]
- assert connection.id == "37ca6ced-58d7-4dcf-99dc-f0a85223cbef"
- assert connection.connection_type == "postgres"
- assert connection.server_address == "localhost"
- assert connection.server_port == "5432"
- assert connection.username == "pgadmin"
+ for i, populate_connections_xml in enumerate(
+ (VIRTUAL_CONNECTION_POPULATE_CONNECTIONS, VIRTUAL_CONNECTION_POPULATE_CONNECTIONS2)
+ ):
+ with self.subTest(i):
+ vconn = VirtualConnectionItem("vconn")
+ vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79"
+ with requests_mock.mock() as m:
+ m.get(f"{self.baseurl}/{vconn.id}/connections", text=populate_connections_xml.read_text())
+ vc_out = self.server.virtual_connections.populate_connections(vconn)
+ connection_list = list(vconn.connections)
+
+ assert vc_out is vconn
+ assert vc_out._connections is not None
+
+ assert len(connection_list) == 1
+ connection = connection_list[0]
+ assert connection.id == "37ca6ced-58d7-4dcf-99dc-f0a85223cbef"
+ assert connection.connection_type == "postgres"
+ assert connection.server_address == "localhost"
+ assert connection.server_port == "5432"
+ assert connection.username == "pgadmin"
def test_virtual_connection_update_connection_db_connection(self):
vconn = VirtualConnectionItem("vconn")
diff --git a/test/test_workbook.py b/test/test_workbook.py
index 0aa52f50d..f3c2dd147 100644
--- a/test/test_workbook.py
+++ b/test/test_workbook.py
@@ -12,7 +12,7 @@
import tableauserverclient as TSC
from tableauserverclient.datetime_helpers import format_datetime
from tableauserverclient.models import UserItem, GroupItem, PermissionsRule
-from tableauserverclient.server.endpoint.exceptions import InternalServerError
+from tableauserverclient.server.endpoint.exceptions import InternalServerError, UnsupportedAttributeError
from tableauserverclient.server.request_factory import RequestFactory
from ._utils import asset
@@ -450,6 +450,49 @@ def test_populate_pdf(self) -> None:
self.server.workbooks.populate_pdf(single_workbook, req_option)
self.assertEqual(response, single_workbook.pdf)
+ def test_populate_pdf_unsupported(self) -> None:
+ self.server.version = "3.4"
+ self.baseurl = self.server.workbooks.baseurl
+ with requests_mock.mock() as m:
+ m.get(
+ self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/pdf?type=a5&orientation=landscape",
+ content=b"",
+ )
+ single_workbook = TSC.WorkbookItem("test")
+ single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2"
+
+ type = TSC.PDFRequestOptions.PageType.A5
+ orientation = TSC.PDFRequestOptions.Orientation.Landscape
+ req_option = TSC.PDFRequestOptions(type, orientation)
+ req_option.vf("Region", "West")
+
+ with self.assertRaises(UnsupportedAttributeError):
+ self.server.workbooks.populate_pdf(single_workbook, req_option)
+
+ def test_populate_pdf_vf_dims(self) -> None:
+ self.server.version = "3.23"
+ self.baseurl = self.server.workbooks.baseurl
+ with open(POPULATE_PDF, "rb") as f:
+ response = f.read()
+ with requests_mock.mock() as m:
+ m.get(
+ self.baseurl
+ + "/1f951daf-4061-451a-9df1-69a8062664f2/pdf?type=a5&orientation=landscape&vf_Region=West&vizWidth=1920&vizHeight=1080",
+ content=response,
+ )
+ single_workbook = TSC.WorkbookItem("test")
+ single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2"
+
+ type = TSC.PDFRequestOptions.PageType.A5
+ orientation = TSC.PDFRequestOptions.Orientation.Landscape
+ req_option = TSC.PDFRequestOptions(type, orientation)
+ req_option.vf("Region", "West")
+ req_option.viz_width = 1920
+ req_option.viz_height = 1080
+
+ self.server.workbooks.populate_pdf(single_workbook, req_option)
+ self.assertEqual(response, single_workbook.pdf)
+
def test_populate_powerpoint(self) -> None:
self.server.version = "3.8"
self.baseurl = self.server.workbooks.baseurl
@@ -457,13 +500,15 @@ def test_populate_powerpoint(self) -> None:
response = f.read()
with requests_mock.mock() as m:
m.get(
- self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/powerpoint",
+ self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/powerpoint?maxAge=1",
content=response,
)
single_workbook = TSC.WorkbookItem("test")
single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2"
- self.server.workbooks.populate_powerpoint(single_workbook)
+ ro = TSC.PPTXRequestOptions(maxage=1)
+
+ self.server.workbooks.populate_powerpoint(single_workbook, ro)
self.assertEqual(response, single_workbook.powerpoint)
def test_populate_preview_image(self) -> None: