From e933529480efb6718c223f06f336a8008e9c2ef1 Mon Sep 17 00:00:00 2001 From: Jordan Woods <13803242+jorwoods@users.noreply.github.com> Date: Fri, 29 Aug 2025 07:55:00 -0500 Subject: [PATCH 1/3] chore: make datasource typing more specific Use overloads to narrow return types of DatasourceEndpoint to reflect what users actually pass in. --- .../server/endpoint/datasources_endpoint.py | 71 ++++++++++++++----- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/tableauserverclient/server/endpoint/datasources_endpoint.py b/tableauserverclient/server/endpoint/datasources_endpoint.py index ba242c8e..a1546f67 100644 --- a/tableauserverclient/server/endpoint/datasources_endpoint.py +++ b/tableauserverclient/server/endpoint/datasources_endpoint.py @@ -6,7 +6,7 @@ from contextlib import closing from pathlib import Path -from typing import Literal, Optional, TYPE_CHECKING, Union, overload +from typing import Literal, Optional, TYPE_CHECKING, TypeVar, Union, overload from collections.abc import Iterable, Mapping, Sequence from tableauserverclient.helpers.headers import fix_filename @@ -50,7 +50,6 @@ FileObject = Union[io.BufferedReader, io.BytesIO] PathOrFile = Union[FilePath, FileObject] -FilePath = Union[str, os.PathLike] FileObjectR = Union[io.BufferedReader, io.BytesIO] FileObjectW = Union[io.BufferedWriter, io.BytesIO] PathOrFileR = Union[FilePath, FileObjectR] @@ -191,16 +190,34 @@ def delete(self, datasource_id: str) -> None: self.delete_request(url) logger.info(f"Deleted single datasource (ID: {datasource_id})") + T = TypeVar("T", bound=FileObjectW) + + @overload + def download( + self, + datasource_id: str, + filepath: T, + include_extract: bool = True, + ) -> T: ... + + @overload + def download( + self, + datasource_id: str, + filepath: Optional[FilePath] = None, + include_extract: bool = True, + ) -> str: ... + # Download 1 datasource by id @api(version="2.0") @parameter_added_in(no_extract="2.5") @parameter_added_in(include_extract="2.5") def download( self, - datasource_id: str, - filepath: Optional[PathOrFileW] = None, - include_extract: bool = True, - ) -> PathOrFileW: + datasource_id, + filepath, + include_extract=True, + ): """ Downloads the specified data source from a site. The data source is downloaded as a .tdsx file. @@ -479,13 +496,13 @@ def publish( @parameter_added_in(as_job="3.0") def publish( self, - datasource_item: DatasourceItem, - file: PathOrFileR, - mode: str, - connection_credentials: Optional[ConnectionCredentials] = None, - connections: Optional[Sequence[ConnectionItem]] = None, - as_job: bool = False, - ) -> Union[DatasourceItem, JobItem]: + datasource_item, + file, + mode, + connection_credentials=None, + connections=None, + as_job=False, + ): """ Publishes a data source to a server, or appends data to an existing data source. @@ -898,15 +915,35 @@ def _get_datasource_revisions( revisions = RevisionItem.from_response(server_response.content, self.parent_srv.namespace, datasource_item) return revisions - # Download 1 datasource revision by revision number - @api(version="2.3") + T = TypeVar("T", bound=FileObjectW) + + @overload + def download_revision( + self, + datasource_id: str, + revision_number: Optional[str], + filepath: T, + include_extract: bool = True, + ) -> T: ... + + @overload def download_revision( self, datasource_id: str, revision_number: Optional[str], - filepath: Optional[PathOrFileW] = None, + filepath: Optional[FilePath] = None, include_extract: bool = True, - ) -> PathOrFileW: + ) -> str: ... + + # Download 1 datasource revision by revision number + @api(version="2.3") + def download_revision( + self, + datasource_id, + revision_number, + filepath, + include_extract, + ): """ 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 From 772ffa8883773566af7b6843fb99c1864997ffce Mon Sep 17 00:00:00 2001 From: Jordan Woods <13803242+jorwoods@users.noreply.github.com> Date: Fri, 29 Aug 2025 08:11:56 -0500 Subject: [PATCH 2/3] fix: restore defaults --- tableauserverclient/server/endpoint/datasources_endpoint.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tableauserverclient/server/endpoint/datasources_endpoint.py b/tableauserverclient/server/endpoint/datasources_endpoint.py index a1546f67..4ac791c1 100644 --- a/tableauserverclient/server/endpoint/datasources_endpoint.py +++ b/tableauserverclient/server/endpoint/datasources_endpoint.py @@ -215,7 +215,7 @@ def download( def download( self, datasource_id, - filepath, + filepath=None, include_extract=True, ): """ @@ -941,8 +941,8 @@ def download_revision( self, datasource_id, revision_number, - filepath, - include_extract, + filepath=None, + include_extract=True, ): """ Downloads a specific version of a data source prior to the current one From 410811f0a06e58179b41d751c5ad8d499564645c Mon Sep 17 00:00:00 2001 From: Jordan Woods <13803242+jorwoods@users.noreply.github.com> Date: Wed, 3 Sep 2025 21:09:51 -0500 Subject: [PATCH 3/3] chore: make update_hyper_data actions more specific --- tableauserverclient/models/datasource_item.py | 2 +- .../server/endpoint/datasources_endpoint.py | 44 +++++++++++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/tableauserverclient/models/datasource_item.py b/tableauserverclient/models/datasource_item.py index 5501ee33..2813c370 100644 --- a/tableauserverclient/models/datasource_item.py +++ b/tableauserverclient/models/datasource_item.py @@ -490,7 +490,7 @@ def _set_values( self._owner = owner @classmethod - def from_response(cls, resp: str, ns: dict) -> list["DatasourceItem"]: + def from_response(cls, resp: bytes, ns: dict) -> list["DatasourceItem"]: all_datasource_items = list() parsed_response = fromstring(resp) all_datasource_xml = parsed_response.findall(".//t:datasource", namespaces=ns) diff --git a/tableauserverclient/server/endpoint/datasources_endpoint.py b/tableauserverclient/server/endpoint/datasources_endpoint.py index 4ac791c1..f528b373 100644 --- a/tableauserverclient/server/endpoint/datasources_endpoint.py +++ b/tableauserverclient/server/endpoint/datasources_endpoint.py @@ -6,8 +6,8 @@ from contextlib import closing from pathlib import Path -from typing import Literal, Optional, TYPE_CHECKING, TypeVar, Union, overload -from collections.abc import Iterable, Mapping, Sequence +from typing import Literal, Optional, TYPE_CHECKING, TypedDict, TypeVar, Union, overload +from collections.abc import Iterable, Sequence from tableauserverclient.helpers.headers import fix_filename from tableauserverclient.models.dqw_item import DQWItem @@ -56,6 +56,44 @@ PathOrFileW = Union[FilePath, FileObjectW] +HyperActionCondition = TypedDict( + "HyperActionCondition", + { + "op": str, + "target-col": str, + "source-col": str, + }, +) + +HyperActionRow = TypedDict( + "HyperActionRow", + { + "action": Literal[ + "update", + "upsert", + "delete", + ], + "source-table": str, + "target-table": str, + "condition": HyperActionCondition, + }, +) + +HyperActionTable = TypedDict( + "HyperActionTable", + { + "action": Literal[ + "insert", + "replace", + ], + "source-table": str, + "target-table": str, + }, +) + +HyperAction = Union[HyperActionTable, HyperActionRow] + + class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]): def __init__(self, parent_srv: "Server") -> None: super().__init__(parent_srv) @@ -648,7 +686,7 @@ def update_hyper_data( datasource_or_connection_item: Union[DatasourceItem, ConnectionItem, str], *, request_id: str, - actions: Sequence[Mapping], + actions: Sequence[HyperAction], payload: Optional[FilePath] = None, ) -> JobItem: """