Skip to content

Commit 1b292b7

Browse files
committed
feat: project support all fields
1 parent 364f431 commit 1b292b7

File tree

7 files changed

+295
-31
lines changed

7 files changed

+295
-31
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from typing import Optional
2+
import xml.etree.ElementTree as ET
3+
4+
5+
class LocationItem:
6+
class Type:
7+
PersonalSpace = "PersonalSpace"
8+
Project = "Project"
9+
10+
def __init__(self):
11+
self._id: Optional[str] = None
12+
self._type: Optional[str] = None
13+
self._name: Optional[str] = None
14+
15+
def __repr__(self):
16+
return f"{self.__class__.__name__}({self.__dict__!r})"
17+
18+
@property
19+
def id(self) -> Optional[str]:
20+
return self._id
21+
22+
@property
23+
def type(self) -> Optional[str]:
24+
return self._type
25+
26+
@property
27+
def name(self) -> Optional[str]:
28+
return self._name
29+
30+
@classmethod
31+
def from_xml(cls, xml: ET.Element, ns: Optional[dict] = None) -> "LocationItem":
32+
if ns is None:
33+
ns = {}
34+
location = cls()
35+
location._id = xml.get("id", None)
36+
location._type = xml.get("type", None)
37+
location._name = xml.get("name", None)
38+
return location

tableauserverclient/models/project_item.py

Lines changed: 105 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import logging
21
import xml.etree.ElementTree as ET
3-
from typing import Optional
2+
from typing import Optional, overload
43

54
from defusedxml.ElementTree import fromstring
65

76
from tableauserverclient.models.exceptions import UnpopulatedPropertyError
8-
from tableauserverclient.models.property_decorators import property_is_enum, property_not_empty
7+
from tableauserverclient.models.property_decorators import property_is_enum
98

109

1110
class ProjectItem:
@@ -75,6 +74,8 @@ def __init__(
7574
self.parent_id: Optional[str] = parent_id
7675
self._samples: Optional[bool] = samples
7776
self._owner_id: Optional[str] = None
77+
self._top_level_project: Optional[bool] = None
78+
self._writeable: Optional[bool] = None
7879

7980
self._permissions = None
8081
self._default_workbook_permissions = None
@@ -87,6 +88,11 @@ def __init__(
8788
self._default_database_permissions = None
8889
self._default_table_permissions = None
8990

91+
self._project_count: Optional[int] = None
92+
self._workbok_count: Optional[int] = None
93+
self._view_count: Optional[int] = None
94+
self._datasource_count: Optional[int] = None
95+
9096
@property
9197
def content_permissions(self):
9298
return self._content_permissions
@@ -176,25 +182,48 @@ def owner_id(self) -> Optional[str]:
176182
def owner_id(self, value: str) -> None:
177183
self._owner_id = value
178184

185+
@property
186+
def top_level_project(self) -> Optional[bool]:
187+
return self._top_level_project
188+
189+
@property
190+
def writeable(self) -> Optional[bool]:
191+
return self._writeable
192+
193+
@property
194+
def project_count(self) -> Optional[int]:
195+
return self._project_count
196+
197+
@property
198+
def workbok_count(self) -> Optional[int]:
199+
return self._workbok_count
200+
201+
@property
202+
def view_count(self) -> Optional[int]:
203+
return self._view_count
204+
205+
@property
206+
def datasource_count(self) -> Optional[int]:
207+
return self._datasource_count
208+
179209
def is_default(self):
180210
return self.name.lower() == "default"
181211

182-
def _parse_common_tags(self, project_xml, ns):
183-
if not isinstance(project_xml, ET.Element):
184-
project_xml = fromstring(project_xml).find(".//t:project", namespaces=ns)
185-
186-
if project_xml is not None:
187-
(
188-
_,
189-
name,
190-
description,
191-
content_permissions,
192-
parent_id,
193-
) = self._parse_element(project_xml)
194-
self._set_values(None, name, description, content_permissions, parent_id)
195-
return self
196-
197-
def _set_values(self, project_id, name, description, content_permissions, parent_id, owner_id):
212+
def _set_values(
213+
self,
214+
project_id,
215+
name,
216+
description,
217+
content_permissions,
218+
parent_id,
219+
owner_id,
220+
top_level_project,
221+
writeable,
222+
project_count,
223+
workbok_count,
224+
view_count,
225+
datasource_count,
226+
):
198227
if project_id is not None:
199228
self._id = project_id
200229
if name:
@@ -207,6 +236,18 @@ def _set_values(self, project_id, name, description, content_permissions, parent
207236
self.parent_id = parent_id
208237
if owner_id:
209238
self._owner_id = owner_id
239+
if project_count is not None:
240+
self._project_count = project_count
241+
if workbok_count is not None:
242+
self._workbok_count = workbok_count
243+
if view_count is not None:
244+
self._view_count = view_count
245+
if datasource_count is not None:
246+
self._datasource_count = datasource_count
247+
if top_level_project is not None:
248+
self._top_level_project = top_level_project
249+
if writeable is not None:
250+
self._writeable = writeable
210251

211252
def _set_permissions(self, permissions):
212253
self._permissions = permissions
@@ -220,31 +261,68 @@ def _set_default_permissions(self, permissions, content_type):
220261
)
221262

222263
@classmethod
223-
def from_response(cls, resp, ns) -> list["ProjectItem"]:
264+
def from_response(cls, resp: bytes, ns: Optional[dict]) -> list["ProjectItem"]:
224265
all_project_items = list()
225266
parsed_response = fromstring(resp)
226267
all_project_xml = parsed_response.findall(".//t:project", namespaces=ns)
227268

228269
for project_xml in all_project_xml:
229-
project_item = cls.from_xml(project_xml)
270+
project_item = cls.from_xml(project_xml, namespace=ns)
230271
all_project_items.append(project_item)
231272
return all_project_items
232273

233274
@classmethod
234-
def from_xml(cls, project_xml, namespace=None) -> "ProjectItem":
275+
def from_xml(cls, project_xml: ET.Element, namespace: Optional[dict] = None) -> "ProjectItem":
235276
project_item = cls()
236-
project_item._set_values(*cls._parse_element(project_xml))
277+
project_item._set_values(*cls._parse_element(project_xml, namespace))
237278
return project_item
238279

239280
@staticmethod
240-
def _parse_element(project_xml):
281+
def _parse_element(project_xml: ET.Element, namespace: Optional[dict]) -> tuple:
241282
id = project_xml.get("id", None)
242283
name = project_xml.get("name", None)
243284
description = project_xml.get("description", None)
244285
content_permissions = project_xml.get("contentPermissions", None)
245286
parent_id = project_xml.get("parentProjectId", None)
287+
top_level_project = str_to_bool(project_xml.get("topLevelProject", None))
288+
writeable = str_to_bool(project_xml.get("writeable", None))
246289
owner_id = None
247-
for owner in project_xml:
248-
owner_id = owner.get("id", None)
290+
if (owner_elem := project_xml.find(".//t:owner", namespaces=namespace)) is not None:
291+
owner_id = owner_elem.get("id", None)
292+
293+
project_count = None
294+
workbok_count = None
295+
view_count = None
296+
datasource_count = None
297+
if (count_elem := project_xml.find(".//t:contentsCounts", namespaces=namespace)) is not None:
298+
project_count = int(count_elem.get("projectCount", 0))
299+
workbok_count = int(count_elem.get("workbookCount", 0))
300+
view_count = int(count_elem.get("viewCount", 0))
301+
datasource_count = int(count_elem.get("dataSourceCount", 0))
302+
303+
return (
304+
id,
305+
name,
306+
description,
307+
content_permissions,
308+
parent_id,
309+
owner_id,
310+
top_level_project,
311+
writeable,
312+
project_count,
313+
workbok_count,
314+
view_count,
315+
datasource_count,
316+
)
317+
318+
319+
@overload
320+
def str_to_bool(value: str) -> bool: ...
321+
322+
323+
@overload
324+
def str_to_bool(value: None) -> None: ...
325+
249326

250-
return id, name, description, content_permissions, parent_id, owner_id
327+
def str_to_bool(value):
328+
return value.lower() == "true" if value is not None else None

tableauserverclient/server/endpoint/users_endpoint.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def get(self, req_options: Optional[RequestOptions] = None) -> tuple[list[UserIt
8787

8888
if req_options is None:
8989
req_options = RequestOptions()
90-
req_options._all_fields = True
90+
req_options.all_fields = True
9191

9292
url = self.baseurl
9393
server_response = self.get_request(url, req_options)

tableauserverclient/server/request_options.py

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ def __init__(self, pagenumber=1, pagesize=None):
6262
self.pagesize = pagesize or config.PAGE_SIZE
6363
self.sort = set()
6464
self.filter = set()
65+
self.fields = set()
6566
# This is private until we expand all of our parsers to handle the extra fields
66-
self._all_fields = False
67+
self.all_fields = False
6768

6869
def get_query_params(self) -> dict:
6970
params = {}
@@ -75,12 +76,14 @@ def get_query_params(self) -> dict:
7576
filter_options = (str(filter_item) for filter_item in self.filter)
7677
ordered_filter_options = sorted(filter_options)
7778
params["filter"] = ",".join(ordered_filter_options)
78-
if self._all_fields:
79+
if self.all_fields:
7980
params["fields"] = "_all_"
8081
if self.pagenumber:
8182
params["pageNumber"] = self.pagenumber
8283
if self.pagesize:
8384
params["pageSize"] = self.pagesize
85+
if self.fields:
86+
params["fields"] = ",".join(self.fields)
8487
return params
8588

8689
def page_size(self, page_size):
@@ -181,6 +184,116 @@ class Direction:
181184
Desc = "desc"
182185
Asc = "asc"
183186

187+
class SelectFields:
188+
class Common:
189+
All = "_all_"
190+
Default = "_default_"
191+
192+
class ContentsCounts:
193+
ProjectCount = "contentsCounts.projectCount"
194+
ViewCount = "contentsCounts.viewCount"
195+
DatasourceCount = "contentsCounts.datasourceCount"
196+
WorkbookCount = "contentsCounts.workbookCount"
197+
198+
class Datasource:
199+
ContentUrl = "datasource.contentUrl"
200+
ID = "datasource.id"
201+
Name = "datasource.name"
202+
Type = "datasource.type"
203+
Description = "datasource.description"
204+
CreatedAt = "datasource.createdAt"
205+
UpdatedAt = "datasource.updatedAt"
206+
EncryptExtracts = "datasource.encryptExtracts"
207+
IsCertified = "datasource.isCertified"
208+
UseRemoteQueryAgent = "datasource.useRemoteQueryAgent"
209+
WebPageURL = "datasource.webpageUrl"
210+
Size = "datasource.size"
211+
Tag = "datasource.tag"
212+
FavoritesTotal = "datasource.favoritesTotal"
213+
DatabaseName = "datasource.databaseName"
214+
ConnectedWorkbooksCount = "datasource.connectedWorkbooksCount"
215+
HasAlert = "datasource.hasAlert"
216+
HasExtracts = "datasource.hasExtracts"
217+
IsPublished = "datasource.isPublished"
218+
ServerName = "datasource.serverName"
219+
220+
class Favorite:
221+
Label = "favorite.label"
222+
ParentProjectName = "favorite.parentProjectName"
223+
TargetOwnerName = "favorite.targetOwnerName"
224+
225+
class Group:
226+
ID = "group.id"
227+
Name = "group.name"
228+
DomainName = "group.domainName"
229+
UserCount = "group.userCount"
230+
MinimumSiteRole = "group.minimumSiteRole"
231+
232+
class Job:
233+
ID = "job.id"
234+
Status = "job.status"
235+
CreatedAt = "job.createdAt"
236+
StartedAt = "job.startedAt"
237+
EndedAt = "job.endedAt"
238+
Priority = "job.priority"
239+
JobType = "job.jobType"
240+
Title = "job.title"
241+
Subtitle = "job.subtitle"
242+
243+
class Owner:
244+
ID = "owner.id"
245+
Name = "owner.name"
246+
FullName = "owner.fullName"
247+
SiteRole = "owner.siteRole"
248+
LastLogin = "owner.lastLogin"
249+
Email = "owner.email"
250+
251+
class Project:
252+
ID = "project.id"
253+
Name = "project.name"
254+
Description = "project.description"
255+
CreatedAt = "project.createdAt"
256+
UpdatedAt = "project.updatedAt"
257+
ContentPermissions = "project.contentPermissions"
258+
ParentProjectID = "project.parentProjectId"
259+
TopLevelProject = "project.topLevelProject"
260+
Writeable = "project.writeable"
261+
262+
class User:
263+
ExternalAuthUserId = "user.externalAuthUserId"
264+
ID = "user.id"
265+
Name = "user.name"
266+
SiteRole = "user.siteRole"
267+
LastLogin = "user.lastLogin"
268+
FullName = "user.fullName"
269+
Email = "user.email"
270+
AuthSetting = "user.authSetting"
271+
272+
class View:
273+
ID = "view.id"
274+
Name = "view.name"
275+
ContentUrl = "view.contentUrl"
276+
CreatedAt = "view.createdAt"
277+
UpdatedAt = "view.updatedAt"
278+
Tags = "view.tags"
279+
SheetType = "view.sheetType"
280+
Usage = "view.usage"
281+
282+
class Workbook:
283+
ID = "workbook.id"
284+
Description = "workbook.description"
285+
Name = "workbook.name"
286+
ContentUrl = "workbook.contentUrl"
287+
ShowTabs = "workbook.showTabs"
288+
Size = "workbook.size"
289+
CreatedAt = "workbook.createdAt"
290+
UpdatedAt = "workbook.updatedAt"
291+
SheetCount = "workbook.sheetCount"
292+
HasExtracts = "workbook.hasExtracts"
293+
Tags = "workbook.tags"
294+
WebpageUrl = "workbook.webpageUrl"
295+
DefaultViewId = "workbook.defaultViewId"
296+
184297

185298
"""
186299
These options can be used by methods that are fetching data exported from a specific content item
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<ns0:tsResponse xmlns:ns0="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api https://help.tableau.com/samples/en-us/rest_api/ts-api_3_25.xsd">
2+
<ns0:pagination pageNumber="1" pageSize="100" totalAvailable="3" />
3+
<ns0:projects>
4+
<ns0:project id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" name="Samples" description="This project includes automatically uploaded samples." topLevelProject="true" writeable="true" createdAt="2024-02-14T04:42:03Z" updatedAt="2024-02-14T04:42:03Z" contentPermissions="ManagedByOwner">
5+
<ns0:owner email="[email protected]" fullName="Bob" id="ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" lastLogin="2025-02-04T06:39:20Z" name="Bob" siteRole="SiteAdministratorCreator" />
6+
<ns0:contentsCounts projectCount="0" workbookCount="2" viewCount="17" datasourceCount="1" />
7+
</ns0:project>
8+
</ns0:projects>
9+
</ns0:tsResponse>

0 commit comments

Comments
 (0)