Skip to content

Commit fd187ba

Browse files
authored
feat: support collections in favorites (#1647)
* feat: support collections in favorites The API schema shows collections can be returned with favorites. This change adds support for a `CollectionItem`, as well as making the bundled type returned by favorites more specific. * fix: change Self import to make compat with < 3.11 * fix: use parse_datetime --------- Co-authored-by: Jordan Woods <[email protected]>
1 parent 022e6f1 commit fd187ba

File tree

7 files changed

+105
-12
lines changed

7 files changed

+105
-12
lines changed

tableauserverclient/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from tableauserverclient.namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE
33
from tableauserverclient.models import (
44
BackgroundJobItem,
5+
CollectionItem,
56
ColumnItem,
67
ConnectionCredentials,
78
ConnectionItem,
@@ -73,7 +74,7 @@
7374

7475
__all__ = [
7576
"BackgroundJobItem",
76-
"BackgroundJobItem",
77+
"CollectionItem",
7778
"ColumnItem",
7879
"ConnectionCredentials",
7980
"ConnectionItem",

tableauserverclient/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from tableauserverclient.models.collection_item import CollectionItem
12
from tableauserverclient.models.column_item import ColumnItem
23
from tableauserverclient.models.connection_credentials import ConnectionCredentials
34
from tableauserverclient.models.connection_item import ConnectionItem
@@ -53,6 +54,7 @@
5354
from tableauserverclient.models.extract_item import ExtractItem
5455

5556
__all__ = [
57+
"CollectionItem",
5658
"ColumnItem",
5759
"ConnectionCredentials",
5860
"ConnectionItem",
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from datetime import datetime
2+
from typing import Optional
3+
from xml.etree.ElementTree import Element
4+
5+
from defusedxml.ElementTree import fromstring
6+
from typing_extensions import Self
7+
8+
from tableauserverclient.datetime_helpers import parse_datetime
9+
from tableauserverclient.models.user_item import UserItem
10+
11+
12+
class CollectionItem:
13+
def __init__(self) -> None:
14+
self.id: Optional[str] = None
15+
self.name: Optional[str] = None
16+
self.description: Optional[str] = None
17+
self.created_at: Optional[datetime] = None
18+
self.updated_at: Optional[datetime] = None
19+
self.owner: Optional[UserItem] = None
20+
self.total_item_count: Optional[int] = None
21+
self.permissioned_item_count: Optional[int] = None
22+
self.visibility: Optional[str] = None # Assuming visibility is a string, adjust as necessary
23+
24+
@classmethod
25+
def from_response(cls, response: bytes, ns) -> list[Self]:
26+
parsed_response = fromstring(response)
27+
28+
collection_elements = parsed_response.findall(".//t:collection", namespaces=ns)
29+
if not collection_elements:
30+
raise ValueError("No collection element found in the response")
31+
32+
collections = [cls.from_xml(c, ns) for c in collection_elements]
33+
return collections
34+
35+
@classmethod
36+
def from_xml(cls, xml: Element, ns) -> Self:
37+
collection_item = cls()
38+
collection_item.id = xml.get("id")
39+
collection_item.name = xml.get("name")
40+
collection_item.description = xml.get("description")
41+
collection_item.created_at = parse_datetime(xml.get("createdAt"))
42+
collection_item.updated_at = parse_datetime(xml.get("updatedAt"))
43+
owner_element = xml.find(".//t:owner", namespaces=ns)
44+
if owner_element is not None:
45+
collection_item.owner = UserItem.from_xml(owner_element, ns)
46+
else:
47+
collection_item.owner = None
48+
collection_item.total_item_count = int(xml.get("totalItemCount", 0))
49+
collection_item.permissioned_item_count = int(xml.get("permissionedItemCount", 0))
50+
collection_item.visibility = xml.get("visibility")
51+
52+
return collection_item

tableauserverclient/models/favorites_item.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import logging
22

3-
from typing import Union
3+
from typing import TypedDict, Union
44
from defusedxml.ElementTree import fromstring
5-
6-
from tableauserverclient.models.tableau_types import TableauItem
5+
from tableauserverclient.models.collection_item import CollectionItem
76
from tableauserverclient.models.datasource_item import DatasourceItem
87
from tableauserverclient.models.flow_item import FlowItem
98
from tableauserverclient.models.project_item import ProjectItem
@@ -13,16 +12,22 @@
1312

1413
from tableauserverclient.helpers.logging import logger
1514

16-
FavoriteType = dict[
17-
str,
18-
list[TableauItem],
19-
]
15+
16+
class FavoriteType(TypedDict):
17+
collections: list[CollectionItem]
18+
datasources: list[DatasourceItem]
19+
flows: list[FlowItem]
20+
projects: list[ProjectItem]
21+
metrics: list[MetricItem]
22+
views: list[ViewItem]
23+
workbooks: list[WorkbookItem]
2024

2125

2226
class FavoriteItem:
2327
@classmethod
2428
def from_response(cls, xml: Union[str, bytes], namespace: dict) -> FavoriteType:
2529
favorites: FavoriteType = {
30+
"collections": [],
2631
"datasources": [],
2732
"flows": [],
2833
"projects": [],
@@ -32,6 +37,7 @@ def from_response(cls, xml: Union[str, bytes], namespace: dict) -> FavoriteType:
3237
}
3338
parsed_response = fromstring(xml)
3439

40+
collections_xml = parsed_response.findall(".//t:favorite/t:collection", namespace)
3541
datasources_xml = parsed_response.findall(".//t:favorite/t:datasource", namespace)
3642
flows_xml = parsed_response.findall(".//t:favorite/t:flow", namespace)
3743
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:
4046
workbooks_xml = parsed_response.findall(".//t:favorite/t:workbook", namespace)
4147

4248
logger.debug(
43-
"ds: {}, flows: {}, metrics: {}, projects: {}, views: {}, wbs: {}".format(
49+
"ds: {}, flows: {}, metrics: {}, projects: {}, views: {}, wbs: {}, collections: {}".format(
4450
len(datasources_xml),
4551
len(flows_xml),
4652
len(metrics_xml),
4753
len(projects_xml),
4854
len(views_xml),
4955
len(workbooks_xml),
56+
len(collections_xml),
5057
)
5158
)
5259
for datasource in datasources_xml:
@@ -85,5 +92,11 @@ def from_response(cls, xml: Union[str, bytes], namespace: dict) -> FavoriteType:
8592
logger.debug(fav_workbook)
8693
favorites["workbooks"].append(fav_workbook)
8794

95+
for collection in collections_xml:
96+
fav_collection = CollectionItem.from_xml(collection, namespace)
97+
if fav_collection:
98+
logger.debug(fav_collection)
99+
favorites["collections"].append(fav_collection)
100+
88101
logger.debug(favorites)
89102
return favorites

tableauserverclient/models/user_item.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
if TYPE_CHECKING:
1919
from tableauserverclient.server import Pager
20+
from tableauserverclient.models.favorites_item import FavoriteType
2021

2122

2223
class UserItem:
@@ -131,7 +132,7 @@ def __init__(
131132
self._id: Optional[str] = None
132133
self._last_login: Optional[datetime] = None
133134
self._workbooks = None
134-
self._favorites: Optional[dict[str, list]] = None
135+
self._favorites: Optional["FavoriteType"] = None
135136
self._groups = None
136137
self.email: Optional[str] = None
137138
self.fullname: Optional[str] = None
@@ -218,7 +219,7 @@ def workbooks(self) -> "Pager":
218219
return self._workbooks()
219220

220221
@property
221-
def favorites(self) -> dict[str, list]:
222+
def favorites(self) -> "FavoriteType":
222223
if self._favorites is None:
223224
error = "User item must be populated with favorites first."
224225
raise UnpopulatedPropertyError(error)

test/assets/favorites_get.xml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,17 @@
4343
<tags />
4444
</datasource>
4545
</favorite>
46+
<favorite>
47+
<collection id="8c57cb8a-d65f-4a32-813e-5a3f86e8f94e"
48+
name="sample collection"
49+
description="description for sample collection"
50+
totalItemCount="3"
51+
permissionedItemCount="2"
52+
visibility="Private"
53+
createdAt="2016-08-11T21:22:40Z"
54+
updatedAt="2016-08-11T21:34:17Z">
55+
<owner id="5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" />
56+
</collection>
57+
</favorite>
4658
</favorites>
47-
</tsResponse>
59+
</tsResponse>

test/test_favorites.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import requests_mock
44

55
import tableauserverclient as TSC
6+
from tableauserverclient.datetime_helpers import parse_datetime
67
from ._utils import read_xml_asset
78

89
GET_FAVORITES_XML = "favorites_get.xml"
@@ -48,6 +49,17 @@ def test_get(self) -> None:
4849
self.assertEqual(datasource.id, "e76a1461-3b1d-4588-bf1b-17551a879ad9")
4950
self.assertEqual(project.id, "1d0304cd-3796-429f-b815-7258370b9b74")
5051

52+
collection = self.user.favorites["collections"][0]
53+
54+
assert collection.id == "8c57cb8a-d65f-4a32-813e-5a3f86e8f94e"
55+
assert collection.name == "sample collection"
56+
assert collection.description == "description for sample collection"
57+
assert collection.total_item_count == 3
58+
assert collection.permissioned_item_count == 2
59+
assert collection.visibility == "Private"
60+
assert collection.created_at == parse_datetime("2016-08-11T21:22:40Z")
61+
assert collection.updated_at == parse_datetime("2016-08-11T21:34:17Z")
62+
5163
def test_add_favorite_workbook(self) -> None:
5264
response_xml = read_xml_asset(ADD_FAVORITE_WORKBOOK_XML)
5365
workbook = TSC.WorkbookItem("")

0 commit comments

Comments
 (0)