Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add fields:_all_ support #1563

Open
wants to merge 15 commits into
base: development
Choose a base branch
from
2 changes: 2 additions & 0 deletions tableauserverclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
LinkedTaskItem,
LinkedTaskStepItem,
LinkedTaskFlowRunItem,
LocationItem,
MetricItem,
MonthlyInterval,
PaginationItem,
Expand Down Expand Up @@ -101,6 +102,7 @@
"LinkedTaskFlowRunItem",
"LinkedTaskItem",
"LinkedTaskStepItem",
"LocationItem",
"MetricItem",
"MissingRequiredFieldError",
"MonthlyInterval",
Expand Down
26 changes: 25 additions & 1 deletion tableauserverclient/helpers/strings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from defusedxml.ElementTree import fromstring, tostring
from functools import singledispatch
from typing import TypeVar
from typing import TypeVar, overload


# the redact method can handle either strings or bytes, but it can't mix them.
Expand Down Expand Up @@ -41,3 +41,27 @@ def _(xml: str) -> str:
@redact_xml.register # type: ignore[no-redef]
def _(xml: bytes) -> bytes:
return _redact_any_type(bytearray(xml), b"password", b"..[redacted]")


@overload
def nullable_str_to_int(value: None) -> None: ...


@overload
def nullable_str_to_int(value: str) -> int: ...


def nullable_str_to_int(value):
return int(value) if value is not None else None


@overload
def nullable_str_to_bool(value: None) -> None: ...


@overload
def nullable_str_to_bool(value: str) -> bool: ...


def nullable_str_to_bool(value):
return str(value).lower() == "true" if value is not None else None
2 changes: 2 additions & 0 deletions tableauserverclient/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
LinkedTaskStepItem,
LinkedTaskFlowRunItem,
)
from tableauserverclient.models.location_item import LocationItem
from tableauserverclient.models.metric_item import MetricItem
from tableauserverclient.models.pagination_item import PaginationItem
from tableauserverclient.models.permissions_item import PermissionsRule, Permission
Expand Down Expand Up @@ -75,6 +76,7 @@
"MonthlyInterval",
"HourlyInterval",
"BackgroundJobItem",
"LocationItem",
"MetricItem",
"PaginationItem",
"Permission",
Expand Down
108 changes: 108 additions & 0 deletions tableauserverclient/models/datasource_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@
from defusedxml.ElementTree import fromstring

from tableauserverclient.datetime_helpers import parse_datetime
from tableauserverclient.helpers.strings import nullable_str_to_bool, nullable_str_to_int
from tableauserverclient.models.connection_item import ConnectionItem
from tableauserverclient.models.exceptions import UnpopulatedPropertyError
from tableauserverclient.models.permissions_item import PermissionsRule
from tableauserverclient.models.project_item import ProjectItem
from tableauserverclient.models.property_decorators import (
property_not_nullable,
property_is_boolean,
property_is_enum,
)
from tableauserverclient.models.revision_item import RevisionItem
from tableauserverclient.models.tag_item import TagItem
from tableauserverclient.models.user_item import UserItem


class DatasourceItem:
Expand All @@ -40,6 +43,9 @@ class DatasourceItem:
specified, it will default to SiteDefault. See REST API Publish
Datasource for more information about ask_data_enablement.

connected_workbooks_count : Optional[int]
The number of workbooks connected to the datasource.

connections : list[ConnectionItem]
The list of data connections (ConnectionItem) for the specified data
source. You must first call the populate_connections method to access
Expand Down Expand Up @@ -67,6 +73,12 @@ class DatasourceItem:
A Boolean value to determine if a datasource should be encrypted or not.
See Extract and Encryption Methods for more information.

favorites_total : Optional[int]
The number of users who have marked the data source as a favorite.

has_alert : Optional[bool]
A Boolean value that indicates whether the data source has an alert.

has_extracts : Optional[bool]
A Boolean value that indicates whether the datasource has extracts.

Expand All @@ -75,20 +87,32 @@ class DatasourceItem:
specific data source or to delete a data source with the get_by_id and
delete methods.

is_published : Optional[bool]
A Boolean value that indicates whether the data source is published.

name : Optional[str]
The name of the data source. If not specified, the name of the published
data source file is used.

owner: Optional[UserItem]
The owner of the data source.

owner_id : Optional[str]
The identifier of the owner of the data source.

project : Optional[ProjectItem]
The project that the data source belongs to.

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.

server_name : Optional[str]
The name of the server where the data source is published.

tags : Optional[set[str]]
The tags (list of strings) that have been added to the data source.

Expand Down Expand Up @@ -143,6 +167,13 @@ def __init__(self, project_id: Optional[str] = None, name: Optional[str] = None)
self.owner_id: Optional[str] = None
self.project_id: Optional[str] = project_id
self.tags: set[str] = set()
self._connected_workbooks_count: Optional[int] = None
self._favorites_total: Optional[int] = None
self._has_alert: Optional[bool] = None
self._is_published: Optional[bool] = None
self._server_name: Optional[str] = None
self._project: Optional[ProjectItem] = None
self._owner: Optional[UserItem] = None

self._permissions = None
self._data_quality_warnings = None
Expand Down Expand Up @@ -274,6 +305,34 @@ def revisions(self) -> list[RevisionItem]:
def size(self) -> Optional[int]:
return self._size

@property
def connected_workbooks_count(self) -> Optional[int]:
return self._connected_workbooks_count

@property
def favorites_total(self) -> Optional[int]:
return self._favorites_total

@property
def has_alert(self) -> Optional[bool]:
return self._has_alert

@property
def is_published(self) -> Optional[bool]:
return self._is_published

@property
def server_name(self) -> Optional[str]:
return self._server_name

@property
def project(self) -> Optional[ProjectItem]:
return self._project

@property
def owner(self) -> Optional[UserItem]:
return self._owner

def _set_connections(self, connections) -> None:
self._connections = connections

Expand Down Expand Up @@ -310,6 +369,13 @@ def _parse_common_elements(self, datasource_xml, ns):
use_remote_query_agent,
webpage_url,
size,
connected_workbooks_count,
favorites_total,
has_alert,
is_published,
server_name,
project,
owner,
) = self._parse_element(datasource_xml, ns)
self._set_values(
ask_data_enablement,
Expand All @@ -331,6 +397,13 @@ def _parse_common_elements(self, datasource_xml, ns):
use_remote_query_agent,
webpage_url,
size,
connected_workbooks_count,
favorites_total,
has_alert,
is_published,
server_name,
project,
owner,
)
return self

Expand All @@ -355,6 +428,13 @@ def _set_values(
use_remote_query_agent,
webpage_url,
size,
connected_workbooks_count,
favorites_total,
has_alert,
is_published,
server_name,
project,
owner,
):
if ask_data_enablement is not None:
self._ask_data_enablement = ask_data_enablement
Expand Down Expand Up @@ -394,6 +474,20 @@ def _set_values(
self._webpage_url = webpage_url
if size is not None:
self._size = int(size)
if connected_workbooks_count is not None:
self._connected_workbooks_count = connected_workbooks_count
if favorites_total is not None:
self._favorites_total = favorites_total
if has_alert is not None:
self._has_alert = has_alert
if is_published is not None:
self._is_published = is_published
if server_name is not None:
self._server_name = server_name
if project is not None:
self._project = project
if owner is not None:
self._owner = owner

@classmethod
def from_response(cls, resp: str, ns: dict) -> list["DatasourceItem"]:
Expand Down Expand Up @@ -428,6 +522,11 @@ def _parse_element(datasource_xml: ET.Element, ns: dict) -> tuple:
use_remote_query_agent = datasource_xml.get("useRemoteQueryAgent", None)
webpage_url = datasource_xml.get("webpageUrl", None)
size = datasource_xml.get("size", None)
connected_workbooks_count = nullable_str_to_int(datasource_xml.get("connectedWorkbooksCount", None))
favorites_total = nullable_str_to_int(datasource_xml.get("favoritesTotal", None))
has_alert = nullable_str_to_bool(datasource_xml.get("hasAlert", None))
is_published = nullable_str_to_bool(datasource_xml.get("isPublished", None))
server_name = datasource_xml.get("serverName", None)

tags = None
tags_elem = datasource_xml.find(".//t:tags", namespaces=ns)
Expand All @@ -438,12 +537,14 @@ def _parse_element(datasource_xml: ET.Element, ns: dict) -> tuple:
project_name = None
project_elem = datasource_xml.find(".//t:project", namespaces=ns)
if project_elem is not None:
project = ProjectItem.from_xml(project_elem, ns)
project_id = project_elem.get("id", None)
project_name = project_elem.get("name", None)

owner_id = None
owner_elem = datasource_xml.find(".//t:owner", namespaces=ns)
if owner_elem is not None:
owner = UserItem.from_xml(owner_elem, ns)
owner_id = owner_elem.get("id", None)

ask_data_enablement = None
Expand Down Expand Up @@ -471,4 +572,11 @@ def _parse_element(datasource_xml: ET.Element, ns: dict) -> tuple:
use_remote_query_agent,
webpage_url,
size,
connected_workbooks_count,
favorites_total,
has_alert,
is_published,
server_name,
project,
owner,
)
11 changes: 11 additions & 0 deletions tableauserverclient/models/group_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ class GroupItem:
login to a site. When the mode is onSync, a license is granted for group
members each time the domain is synced.

Attributes
----------
user_count: Optional[int]
The number of users in the group.

Examples
--------
>>> # Create a new group item
Expand All @@ -65,6 +70,7 @@ def __init__(self, name=None, domain_name=None) -> None:
self._users: Optional[Callable[..., "Pager"]] = None
self.name: Optional[str] = name
self.domain_name: Optional[str] = domain_name
self._user_count: Optional[int] = None

def __repr__(self):
return f"{self.__class__.__name__}({self.__dict__!r})"
Expand Down Expand Up @@ -118,6 +124,10 @@ def users(self) -> "Pager":
def _set_users(self, users: Callable[..., "Pager"]) -> None:
self._users = users

@property
def user_count(self) -> Optional[int]:
return self._user_count

@classmethod
def from_response(cls, resp, ns) -> list["GroupItem"]:
all_group_items = list()
Expand All @@ -127,6 +137,7 @@ def from_response(cls, resp, ns) -> list["GroupItem"]:
name = group_xml.get("name", None)
group_item = cls(name)
group_item._id = group_xml.get("id", None)
group_item._user_count = int(count) if (count := group_xml.get("userCount", None)) else None

# Domain name is returned in a domain element for some calls
domain_elem = group_xml.find(".//t:domain", namespaces=ns)
Expand Down
53 changes: 53 additions & 0 deletions tableauserverclient/models/location_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import Optional
import xml.etree.ElementTree as ET


class LocationItem:
"""
Details of where an item is located, such as a personal space or project.

Attributes
----------
id : str | None
The ID of the location.

type : str | None
The type of location, such as PersonalSpace or Project.

name : str | None
The name of the location.
"""

class Type:
PersonalSpace = "PersonalSpace"
Project = "Project"

def __init__(self):
self._id: Optional[str] = None
self._type: Optional[str] = None
self._name: Optional[str] = None

def __repr__(self):
return f"{self.__class__.__name__}({self.__dict__!r})"

@property
def id(self) -> Optional[str]:
return self._id

@property
def type(self) -> Optional[str]:
return self._type

@property
def name(self) -> Optional[str]:
return self._name

@classmethod
def from_xml(cls, xml: ET.Element, ns: Optional[dict] = None) -> "LocationItem":
if ns is None:
ns = {}
location = cls()
location._id = xml.get("id", None)
location._type = xml.get("type", None)
location._name = xml.get("name", None)
return location
Loading
Loading