diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py
index c15e1a6eb..b041fcdae 100644
--- a/tableauserverclient/__init__.py
+++ b/tableauserverclient/__init__.py
@@ -2,6 +2,7 @@
from tableauserverclient.namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE
from tableauserverclient.models import (
BackgroundJobItem,
+ CollectionItem,
ColumnItem,
ConnectionCredentials,
ConnectionItem,
@@ -73,7 +74,7 @@
__all__ = [
"BackgroundJobItem",
- "BackgroundJobItem",
+ "CollectionItem",
"ColumnItem",
"ConnectionCredentials",
"ConnectionItem",
diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py
index 5ad7ec1c4..67f6553fd 100644
--- a/tableauserverclient/models/__init__.py
+++ b/tableauserverclient/models/__init__.py
@@ -1,3 +1,4 @@
+from tableauserverclient.models.collection_item import CollectionItem
from tableauserverclient.models.column_item import ColumnItem
from tableauserverclient.models.connection_credentials import ConnectionCredentials
from tableauserverclient.models.connection_item import ConnectionItem
@@ -53,6 +54,7 @@
from tableauserverclient.models.extract_item import ExtractItem
__all__ = [
+ "CollectionItem",
"ColumnItem",
"ConnectionCredentials",
"ConnectionItem",
diff --git a/tableauserverclient/models/collection_item.py b/tableauserverclient/models/collection_item.py
new file mode 100644
index 000000000..4fdb61023
--- /dev/null
+++ b/tableauserverclient/models/collection_item.py
@@ -0,0 +1,52 @@
+from datetime import datetime
+from typing import Optional
+from xml.etree.ElementTree import Element
+
+from defusedxml.ElementTree import fromstring
+from typing_extensions import Self
+
+from tableauserverclient.datetime_helpers import parse_datetime
+from tableauserverclient.models.user_item import UserItem
+
+
+class CollectionItem:
+ def __init__(self) -> None:
+ self.id: Optional[str] = None
+ self.name: Optional[str] = None
+ self.description: Optional[str] = None
+ self.created_at: Optional[datetime] = None
+ self.updated_at: Optional[datetime] = None
+ self.owner: Optional[UserItem] = None
+ self.total_item_count: Optional[int] = None
+ self.permissioned_item_count: Optional[int] = None
+ self.visibility: Optional[str] = None # Assuming visibility is a string, adjust as necessary
+
+ @classmethod
+ def from_response(cls, response: bytes, ns) -> list[Self]:
+ parsed_response = fromstring(response)
+
+ collection_elements = parsed_response.findall(".//t:collection", namespaces=ns)
+ if not collection_elements:
+ raise ValueError("No collection element found in the response")
+
+ collections = [cls.from_xml(c, ns) for c in collection_elements]
+ return collections
+
+ @classmethod
+ def from_xml(cls, xml: Element, ns) -> Self:
+ collection_item = cls()
+ collection_item.id = xml.get("id")
+ collection_item.name = xml.get("name")
+ collection_item.description = xml.get("description")
+ collection_item.created_at = parse_datetime(xml.get("createdAt"))
+ collection_item.updated_at = parse_datetime(xml.get("updatedAt"))
+ owner_element = xml.find(".//t:owner", namespaces=ns)
+ if owner_element is not None:
+ collection_item.owner = UserItem.from_xml(owner_element, ns)
+ else:
+ collection_item.owner = None
+ collection_item.total_item_count = int(xml.get("totalItemCount", 0))
+ collection_item.permissioned_item_count = int(xml.get("permissionedItemCount", 0))
+ collection_item.visibility = xml.get("visibility")
+
+ return collection_item
diff --git a/tableauserverclient/models/favorites_item.py b/tableauserverclient/models/favorites_item.py
index 4fea280f7..1189efc31 100644
--- a/tableauserverclient/models/favorites_item.py
+++ b/tableauserverclient/models/favorites_item.py
@@ -1,9 +1,8 @@
import logging
-from typing import Union
+from typing import TypedDict, Union
from defusedxml.ElementTree import fromstring
-
-from tableauserverclient.models.tableau_types import TableauItem
+from tableauserverclient.models.collection_item import CollectionItem
from tableauserverclient.models.datasource_item import DatasourceItem
from tableauserverclient.models.flow_item import FlowItem
from tableauserverclient.models.project_item import ProjectItem
@@ -13,16 +12,22 @@
from tableauserverclient.helpers.logging import logger
-FavoriteType = dict[
- str,
- list[TableauItem],
-]
+
+class FavoriteType(TypedDict):
+ collections: list[CollectionItem]
+ datasources: list[DatasourceItem]
+ flows: list[FlowItem]
+ projects: list[ProjectItem]
+ metrics: list[MetricItem]
+ views: list[ViewItem]
+ workbooks: list[WorkbookItem]
class FavoriteItem:
@classmethod
def from_response(cls, xml: Union[str, bytes], namespace: dict) -> FavoriteType:
favorites: FavoriteType = {
+ "collections": [],
"datasources": [],
"flows": [],
"projects": [],
@@ -32,6 +37,7 @@ def from_response(cls, xml: Union[str, bytes], namespace: dict) -> FavoriteType:
}
parsed_response = fromstring(xml)
+ collections_xml = parsed_response.findall(".//t:favorite/t:collection", namespace)
datasources_xml = parsed_response.findall(".//t:favorite/t:datasource", namespace)
flows_xml = parsed_response.findall(".//t:favorite/t:flow", namespace)
metrics_xml = parsed_response.findall(".//t:favorite/t:metric", namespace)
@@ -40,13 +46,14 @@ def from_response(cls, xml: Union[str, bytes], namespace: dict) -> FavoriteType:
workbooks_xml = parsed_response.findall(".//t:favorite/t:workbook", namespace)
logger.debug(
- "ds: {}, flows: {}, metrics: {}, projects: {}, views: {}, wbs: {}".format(
+ "ds: {}, flows: {}, metrics: {}, projects: {}, views: {}, wbs: {}, collections: {}".format(
len(datasources_xml),
len(flows_xml),
len(metrics_xml),
len(projects_xml),
len(views_xml),
len(workbooks_xml),
+ len(collections_xml),
)
)
for datasource in datasources_xml:
@@ -85,5 +92,11 @@ def from_response(cls, xml: Union[str, bytes], namespace: dict) -> FavoriteType:
logger.debug(fav_workbook)
favorites["workbooks"].append(fav_workbook)
+ for collection in collections_xml:
+ fav_collection = CollectionItem.from_xml(collection, namespace)
+ if fav_collection:
+ logger.debug(fav_collection)
+ favorites["collections"].append(fav_collection)
+
logger.debug(favorites)
return favorites
diff --git a/tableauserverclient/models/user_item.py b/tableauserverclient/models/user_item.py
index c995b4e07..8b2dd3dd6 100644
--- a/tableauserverclient/models/user_item.py
+++ b/tableauserverclient/models/user_item.py
@@ -17,6 +17,7 @@
if TYPE_CHECKING:
from tableauserverclient.server import Pager
+ from tableauserverclient.models.favorites_item import FavoriteType
class UserItem:
@@ -131,7 +132,7 @@ def __init__(
self._id: Optional[str] = None
self._last_login: Optional[datetime] = None
self._workbooks = None
- self._favorites: Optional[dict[str, list]] = None
+ self._favorites: Optional["FavoriteType"] = None
self._groups = None
self.email: Optional[str] = None
self.fullname: Optional[str] = None
@@ -218,7 +219,7 @@ def workbooks(self) -> "Pager":
return self._workbooks()
@property
- def favorites(self) -> dict[str, list]:
+ def favorites(self) -> "FavoriteType":
if self._favorites is None:
error = "User item must be populated with favorites first."
raise UnpopulatedPropertyError(error)
diff --git a/test/assets/favorites_get.xml b/test/assets/favorites_get.xml
index 3d2e2ee6a..8fd780b1d 100644
--- a/test/assets/favorites_get.xml
+++ b/test/assets/favorites_get.xml
@@ -43,5 +43,17 @@
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/test/test_favorites.py b/test/test_favorites.py
index 87332d70f..e0b701953 100644
--- a/test/test_favorites.py
+++ b/test/test_favorites.py
@@ -3,6 +3,7 @@
import requests_mock
import tableauserverclient as TSC
+from tableauserverclient.datetime_helpers import parse_datetime
from ._utils import read_xml_asset
GET_FAVORITES_XML = "favorites_get.xml"
@@ -48,6 +49,17 @@ def test_get(self) -> None:
self.assertEqual(datasource.id, "e76a1461-3b1d-4588-bf1b-17551a879ad9")
self.assertEqual(project.id, "1d0304cd-3796-429f-b815-7258370b9b74")
+ collection = self.user.favorites["collections"][0]
+
+ assert collection.id == "8c57cb8a-d65f-4a32-813e-5a3f86e8f94e"
+ assert collection.name == "sample collection"
+ assert collection.description == "description for sample collection"
+ assert collection.total_item_count == 3
+ assert collection.permissioned_item_count == 2
+ assert collection.visibility == "Private"
+ assert collection.created_at == parse_datetime("2016-08-11T21:22:40Z")
+ assert collection.updated_at == parse_datetime("2016-08-11T21:34:17Z")
+
def test_add_favorite_workbook(self) -> None:
response_xml = read_xml_asset(ADD_FAVORITE_WORKBOOK_XML)
workbook = TSC.WorkbookItem("")