diff --git a/samples/explore_favorites.py b/samples/explore_favorites.py new file mode 100644 index 000000000..243e91954 --- /dev/null +++ b/samples/explore_favorites.py @@ -0,0 +1,85 @@ +# This script demonstrates how to get all favorites, or add/delete a favorite. + +import argparse +import logging +import tableauserverclient as TSC +from tableauserverclient import Resource + + +def main(): + parser = argparse.ArgumentParser(description="Explore favoriting functions supported by the Server API.") + # Common options; please keep those in sync across all samples + parser.add_argument("--server", "-s", help="server address") + parser.add_argument("--site", "-S", help="site name") + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") + parser.add_argument( + "--logging-level", + "-l", + choices=["debug", "info", "error"], + default="error", + help="desired logging level (set to error by default)", + ) + + args = parser.parse_args() + + # Set logging level based on user input, or error by default + logging_level = getattr(logging, args.logging_level.upper()) + logging.basicConfig(level=logging_level) + + # SIGN IN + tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site) + server = TSC.Server(args.server, use_server_version=True) + with server.auth.sign_in(tableau_auth): + print(server) + my_workbook = None + my_view = None + my_datasource = None + + # get all favorites on site for the logged on user + user: TSC.UserItem = TSC.UserItem() + user.id = server.user_id + print("Favorites for user: {}".format(user.id)) + server.favorites.get(user) + print(user.favorites) + + # get list of workbooks + all_workbook_items, pagination_item = server.workbooks.get() + if all_workbook_items is not None and len(all_workbook_items) > 0: + my_workbook: TSC.WorkbookItem = all_workbook_items[0] + server.favorites.add_favorite(server, user, Resource.Workbook.name(), all_workbook_items[0]) + print( + "Workbook added to favorites. Workbook Name: {}, Workbook ID: {}".format( + my_workbook.name, my_workbook.id + ) + ) + views = server.workbooks.populate_views(my_workbook) + if views is not None and len(views) > 0: + my_view = views[0] + server.favorites.add_favorite_view(user, my_view) + print("View added to favorites. View Name: {}, View ID: {}".format(my_view.name, my_view.id)) + + all_datasource_items, pagination_item = server.datasources.get() + if all_datasource_items: + my_datasource = all_datasource_items[0] + server.favorites.add_favorite_datasource(user, my_datasource) + print( + "Datasource added to favorites. Datasource Name: {}, Datasource ID: {}".format( + my_datasource.name, my_datasource.id + ) + ) + + server.favorites.delete_favorite_workbook(user, my_workbook) + print( + "Workbook deleted from favorites. Workbook Name: {}, Workbook ID: {}".format(my_workbook.name, my_workbook.id) + ) + + server.favorites.delete_favorite_view(user, my_view) + print("View deleted from favorites. View Name: {}, View ID: {}".format(my_view.name, my_view.id)) + + server.favorites.delete_favorite_datasource(user, my_datasource) + print( + "Datasource deleted from favorites. Datasource Name: {}, Datasource ID: {}".format( + my_datasource.name, my_datasource.id + ) + ) diff --git a/tableauserverclient/models/datasource_item.py b/tableauserverclient/models/datasource_item.py index dbaa0ff91..7fcc31ebf 100644 --- a/tableauserverclient/models/datasource_item.py +++ b/tableauserverclient/models/datasource_item.py @@ -305,50 +305,16 @@ def from_response(cls, resp: str, ns: Dict) -> List["DatasourceItem"]: all_datasource_xml = parsed_response.findall(".//t:datasource", namespaces=ns) for datasource_xml in all_datasource_xml: - ( - ask_data_enablement, - certified, - certification_note, - content_url, - created_at, - datasource_type, - description, - encrypt_extracts, - has_extracts, - id_, - name, - owner_id, - project_id, - project_name, - tags, - updated_at, - use_remote_query_agent, - webpage_url, - ) = cls._parse_element(datasource_xml, ns) - datasource_item = cls(project_id) - datasource_item._set_values( - ask_data_enablement, - certified, - certification_note, - content_url, - created_at, - datasource_type, - description, - encrypt_extracts, - has_extracts, - id_, - name, - owner_id, - None, - project_name, - tags, - updated_at, - use_remote_query_agent, - webpage_url, - ) + datasource_item = cls.from_xml(datasource_xml, ns) all_datasource_items.append(datasource_item) return all_datasource_items + @classmethod + def from_xml(cls, datasource_xml, ns): + datasource_item = cls() + datasource_item._set_values(*cls._parse_element(datasource_xml, ns)) + return datasource_item + @staticmethod def _parse_element(datasource_xml: ET.Element, ns: Dict) -> Tuple: id_ = datasource_xml.get("id", None) diff --git a/tableauserverclient/models/favorites_item.py b/tableauserverclient/models/favorites_item.py index afa769fd9..f075d1fc3 100644 --- a/tableauserverclient/models/favorites_item.py +++ b/tableauserverclient/models/favorites_item.py @@ -1,74 +1,90 @@ import logging from defusedxml.ElementTree import fromstring +from .tableau_types import TableauItem from .datasource_item import DatasourceItem from .flow_item import FlowItem from .project_item import ProjectItem +from .metric_item import MetricItem from .view_item import ViewItem from .workbook_item import WorkbookItem +from typing import Dict, List logger = logging.getLogger("tableau.models.favorites_item") -from typing import Dict, List, Union FavoriteType = Dict[ str, - List[ - Union[ - DatasourceItem, - ProjectItem, - FlowItem, - ViewItem, - WorkbookItem, - ] - ], + List[TableauItem], ] class FavoriteItem: - class Type: - Workbook: str = "workbook" - Datasource: str = "datasource" - View: str = "view" - Project: str = "project" - Flow: str = "flow" - @classmethod def from_response(cls, xml: str, namespace: Dict) -> FavoriteType: favorites: FavoriteType = { "datasources": [], "flows": [], "projects": [], + "metrics": [], "views": [], "workbooks": [], } - parsed_response = fromstring(xml) - for workbook in parsed_response.findall(".//t:favorite/t:workbook", namespace): - fav_workbook = WorkbookItem("") - fav_workbook._set_values(*fav_workbook._parse_element(workbook, namespace)) - if fav_workbook: - favorites["workbooks"].append(fav_workbook) - for view in parsed_response.findall(".//t:favorite[t:view]", namespace): - fav_views = ViewItem.from_xml_element(view, namespace) - if fav_views: - for fav_view in fav_views: - favorites["views"].append(fav_view) - for datasource in parsed_response.findall(".//t:favorite/t:datasource", namespace): - fav_datasource = DatasourceItem("") - fav_datasource._set_values(*fav_datasource._parse_element(datasource, 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) + projects_xml = parsed_response.findall(".//t:favorite/t:project", namespace) + views_xml = parsed_response.findall(".//t:favorite/t:view", namespace) + workbooks_xml = parsed_response.findall(".//t:favorite/t:workbook", namespace) + + logger.debug( + "ds: {}, flows: {}, metrics: {}, projects: {}, views: {}, wbs: {}".format( + len(datasources_xml), + len(flows_xml), + len(metrics_xml), + len(projects_xml), + len(views_xml), + len(workbooks_xml), + ) + ) + for datasource in datasources_xml: + fav_datasource = DatasourceItem.from_xml(datasource, namespace) if fav_datasource: + logger.debug(fav_datasource) favorites["datasources"].append(fav_datasource) - for project in parsed_response.findall(".//t:favorite/t:project", namespace): - fav_project = ProjectItem("p") - fav_project._set_values(*fav_project._parse_element(project)) - if fav_project: - favorites["projects"].append(fav_project) - for flow in parsed_response.findall(".//t:favorite/t:flow", namespace): - fav_flow = FlowItem("flows") - fav_flow._set_values(*fav_flow._parse_element(flow, namespace)) + + for flow in flows_xml: + fav_flow = FlowItem.from_xml(flow, namespace) if fav_flow: + logger.debug(fav_flow) favorites["flows"].append(fav_flow) + for metric in metrics_xml: + fav_metric = MetricItem.from_xml(metric, namespace) + if fav_metric: + logger.debug(fav_metric) + favorites["metrics"].append(fav_metric) + + for project in projects_xml: + fav_project = ProjectItem.from_xml(project, namespace) + if fav_project: + logger.debug(fav_project) + favorites["projects"].append(fav_project) + + for view in views_xml: + fav_view = ViewItem.from_xml(view, namespace) + if fav_view: + logger.debug(fav_view) + favorites["views"].append(fav_view) + + for workbook in workbooks_xml: + fav_workbook = WorkbookItem.from_xml(workbook, namespace) + if fav_workbook: + logger.debug(fav_workbook) + favorites["workbooks"].append(fav_workbook) + + logger.debug(favorites) return favorites diff --git a/tableauserverclient/models/flow_item.py b/tableauserverclient/models/flow_item.py index f48910602..d543ad8eb 100644 --- a/tableauserverclient/models/flow_item.py +++ b/tableauserverclient/models/flow_item.py @@ -176,34 +176,39 @@ def from_response(cls, resp, ns) -> List["FlowItem"]: all_flow_xml = parsed_response.findall(".//t:flow", namespaces=ns) for flow_xml in all_flow_xml: - ( - id_, - name, - description, - webpage_url, - created_at, - updated_at, - tags, - project_id, - project_name, - owner_id, - ) = cls._parse_element(flow_xml, ns) - flow_item = cls(project_id) - flow_item._set_values( - id_, - name, - description, - webpage_url, - created_at, - updated_at, - tags, - None, - project_name, - owner_id, - ) + flow_item = cls.from_xml(flow_xml, ns) all_flow_items.append(flow_item) return all_flow_items + @classmethod + def from_xml(cls, flow_xml, ns) -> "FlowItem": + ( + id_, + name, + description, + webpage_url, + created_at, + updated_at, + tags, + project_id, + project_name, + owner_id, + ) = cls._parse_element(flow_xml, ns) + flow_item = cls(project_id) + flow_item._set_values( + id_, + name, + description, + webpage_url, + created_at, + updated_at, + tags, + None, + project_name, + owner_id, + ) + return flow_item + @staticmethod def _parse_element(flow_xml, ns): id_ = flow_xml.get("id", None) diff --git a/tableauserverclient/models/metric_item.py b/tableauserverclient/models/metric_item.py index 4adc73fa8..e390d2c4d 100644 --- a/tableauserverclient/models/metric_item.py +++ b/tableauserverclient/models/metric_item.py @@ -5,6 +5,7 @@ from tableauserverclient.datetime_helpers import parse_datetime from .property_decorators import property_is_boolean, property_is_datetime from .tag_item import TagItem +from .permissions_item import Permission class MetricItem(object): @@ -22,6 +23,7 @@ def __init__(self, name: Optional[str] = None): self._view_id: Optional[str] = None self._initial_tags: Set[str] = set() self.tags: Set[str] = set() + self._permissions: Optional[Permission] = None @property def id(self) -> Optional[str]: @@ -110,6 +112,9 @@ def view_id(self) -> Optional[str]: def view_id(self, value: Optional[str]) -> None: self._view_id = value + def _set_permissions(self, permissions): + self._permissions = permissions + def __repr__(self): return "".format(**vars(self)) @@ -123,36 +128,35 @@ def from_response( parsed_response = ET.fromstring(resp) all_metric_xml = parsed_response.findall(".//t:metric", namespaces=ns) for metric_xml in all_metric_xml: - metric_item = cls() - metric_item._id = metric_xml.get("id", None) - metric_item._name = metric_xml.get("name", None) - metric_item._description = metric_xml.get("description", None) - metric_item._webpage_url = metric_xml.get("webpageUrl", None) - metric_item._created_at = parse_datetime(metric_xml.get("createdAt", None)) - metric_item._updated_at = parse_datetime(metric_xml.get("updatedAt", None)) - metric_item._suspended = string_to_bool(metric_xml.get("suspended", "")) - for owner in metric_xml.findall(".//t:owner", namespaces=ns): - metric_item._owner_id = owner.get("id", None) - - for project in metric_xml.findall(".//t:project", namespaces=ns): - metric_item._project_id = project.get("id", None) - metric_item._project_name = project.get("name", None) - - for view in metric_xml.findall(".//t:underlyingView", namespaces=ns): - metric_item._view_id = view.get("id", None) - - tags = set() - tags_elem = metric_xml.find(".//t:tags", namespaces=ns) - if tags_elem is not None: - all_tags = TagItem.from_xml_element(tags_elem, ns) - tags = all_tags - - metric_item.tags = tags - metric_item._initial_tags = tags - - all_metric_items.append(metric_item) + all_metric_items.append(cls.from_xml(metric_xml, ns)) return all_metric_items + @classmethod + def from_xml(cls, metric_xml, ns): + metric_item = cls() + metric_item._id = metric_xml.get("id", None) + metric_item._name = metric_xml.get("name", None) + metric_item._description = metric_xml.get("description", None) + metric_item._webpage_url = metric_xml.get("webpageUrl", None) + metric_item._created_at = parse_datetime(metric_xml.get("createdAt", None)) + metric_item._updated_at = parse_datetime(metric_xml.get("updatedAt", None)) + metric_item._suspended = string_to_bool(metric_xml.get("suspended", "")) + for owner in metric_xml.findall(".//t:owner", namespaces=ns): + metric_item._owner_id = owner.get("id", None) + for project in metric_xml.findall(".//t:project", namespaces=ns): + metric_item._project_id = project.get("id", None) + metric_item._project_name = project.get("name", None) + for view in metric_xml.findall(".//t:underlyingView", namespaces=ns): + metric_item._view_id = view.get("id", None) + tags = set() + tags_elem = metric_xml.find(".//t:tags", namespaces=ns) + if tags_elem is not None: + all_tags = TagItem.from_xml_element(tags_elem, ns) + tags = all_tags + metric_item.tags = tags + metric_item._initial_tags = tags + return metric_item + # Used to convert string represented boolean to a boolean type def string_to_bool(s: str) -> bool: diff --git a/tableauserverclient/models/project_item.py b/tableauserverclient/models/project_item.py index 21358431c..393a7990f 100644 --- a/tableauserverclient/models/project_item.py +++ b/tableauserverclient/models/project_item.py @@ -21,7 +21,7 @@ def __repr__(self): def __init__( self, - name: str, + name: Optional[str] = None, description: Optional[str] = None, content_permissions: Optional[str] = None, parent_id: Optional[str] = None, @@ -104,11 +104,10 @@ def id(self) -> Optional[str]: return self._id @property - def name(self) -> str: + def name(self) -> Optional[str]: return self._name @name.setter - @property_not_empty def name(self, value: str) -> None: self._name = value @@ -173,19 +172,16 @@ def from_response(cls, resp, ns) -> List["ProjectItem"]: all_project_xml = parsed_response.findall(".//t:project", namespaces=ns) for project_xml in all_project_xml: - ( - id, - name, - description, - content_permissions, - parent_id, - owner_id, - ) = cls._parse_element(project_xml) - project_item = cls(name) - project_item._set_values(id, name, description, content_permissions, parent_id, owner_id) + project_item = cls.from_xml(project_xml) all_project_items.append(project_item) return all_project_items + @classmethod + def from_xml(cls, project_xml, namespace=None) -> "ProjectItem": + project_item = cls() + project_item._set_values(*cls._parse_element(project_xml)) + return project_item + @staticmethod def _parse_element(project_xml): id = project_xml.get("id", None) diff --git a/tableauserverclient/models/tableau_types.py b/tableauserverclient/models/tableau_types.py index 9649c7ed9..33fe5eb0c 100644 --- a/tableauserverclient/models/tableau_types.py +++ b/tableauserverclient/models/tableau_types.py @@ -5,23 +5,25 @@ from .project_item import ProjectItem from .view_item import ViewItem from .workbook_item import WorkbookItem +from .metric_item import MetricItem class Resource: Database = "database" Datarole = "datarole" + Table = "table" Datasource = "datasource" Flow = "flow" Lens = "lens" Metric = "metric" Project = "project" - Table = "table" View = "view" Workbook = "workbook" # resource types that have permissions, can be renamed, etc -TableauItem = Union[DatasourceItem, FlowItem, ProjectItem, ViewItem, WorkbookItem] +# todo: refactoring: should actually define TableauItem as an interface and let all these implement it +TableauItem = Union[DatasourceItem, FlowItem, MetricItem, ProjectItem, ViewItem, WorkbookItem] def plural_type(content_type: Resource) -> str: diff --git a/tableauserverclient/models/user_item.py b/tableauserverclient/models/user_item.py index 5e3d18fa6..a12f4b557 100644 --- a/tableauserverclient/models/user_item.py +++ b/tableauserverclient/models/user_item.py @@ -45,6 +45,7 @@ class Roles: class Auth: OpenID = "OpenID" SAML = "SAML" + TableauIDWithMFA = "TableauIDWithMFA" ServerDefault = "ServerDefault" def __init__( diff --git a/tableauserverclient/models/view_item.py b/tableauserverclient/models/view_item.py index 51cceaa9f..ef1fb0e52 100644 --- a/tableauserverclient/models/view_item.py +++ b/tableauserverclient/models/view_item.py @@ -1,5 +1,6 @@ import copy from datetime import datetime +from requests import Response from typing import Callable, Iterator, List, Optional, Set from defusedxml.ElementTree import fromstring @@ -140,7 +141,7 @@ def _set_permissions(self, permissions: Callable[[], List[PermissionsRule]]) -> self._permissions = permissions @classmethod - def from_response(cls, resp, ns, workbook_id="") -> List["ViewItem"]: + def from_response(cls, resp: "Response", ns, workbook_id="") -> List["ViewItem"]: return cls.from_xml_element(fromstring(resp), ns, workbook_id) @classmethod @@ -148,39 +149,38 @@ def from_xml_element(cls, parsed_response, ns, workbook_id="") -> List["ViewItem all_view_items = list() all_view_xml = parsed_response.findall(".//t:view", namespaces=ns) for view_xml in all_view_xml: - view_item = cls() - usage_elem = view_xml.find(".//t:usage", namespaces=ns) - workbook_elem = view_xml.find(".//t:workbook", namespaces=ns) - owner_elem = view_xml.find(".//t:owner", namespaces=ns) - project_elem = view_xml.find(".//t:project", namespaces=ns) - tags_elem = view_xml.find(".//t:tags", namespaces=ns) - view_item._created_at = parse_datetime(view_xml.get("createdAt", None)) - view_item._updated_at = parse_datetime(view_xml.get("updatedAt", None)) - view_item._id = view_xml.get("id", None) - view_item._name = view_xml.get("name", None) - view_item._content_url = view_xml.get("contentUrl", None) - view_item._sheet_type = view_xml.get("sheetType", None) - - if usage_elem is not None: - total_view = usage_elem.get("totalViewCount", None) - if total_view: - view_item._total_views = int(total_view) - - if owner_elem is not None: - view_item._owner_id = owner_elem.get("id", None) - - if project_elem is not None: - view_item._project_id = project_elem.get("id", None) - - if workbook_id: - view_item._workbook_id = workbook_id - elif workbook_elem is not None: - view_item._workbook_id = workbook_elem.get("id", None) - - if tags_elem is not None: - tags = TagItem.from_xml_element(tags_elem, ns) - view_item.tags = tags - view_item._initial_tags = copy.copy(tags) - + view_item = cls.from_xml(view_xml, ns, workbook_id) all_view_items.append(view_item) return all_view_items + + @classmethod + def from_xml(cls, view_xml, ns, workbook_id="") -> "ViewItem": + view_item = cls() + usage_elem = view_xml.find(".//t:usage", namespaces=ns) + workbook_elem = view_xml.find(".//t:workbook", namespaces=ns) + owner_elem = view_xml.find(".//t:owner", namespaces=ns) + project_elem = view_xml.find(".//t:project", namespaces=ns) + tags_elem = view_xml.find(".//t:tags", namespaces=ns) + view_item._created_at = parse_datetime(view_xml.get("createdAt", None)) + view_item._updated_at = parse_datetime(view_xml.get("updatedAt", None)) + view_item._id = view_xml.get("id", None) + view_item._name = view_xml.get("name", None) + view_item._content_url = view_xml.get("contentUrl", None) + view_item._sheet_type = view_xml.get("sheetType", None) + if usage_elem is not None: + total_view = usage_elem.get("totalViewCount", None) + if total_view: + view_item._total_views = int(total_view) + if owner_elem is not None: + view_item._owner_id = owner_elem.get("id", None) + if project_elem is not None: + view_item._project_id = project_elem.get("id", None) + if workbook_id: + view_item._workbook_id = workbook_id + elif workbook_elem is not None: + view_item._workbook_id = workbook_elem.get("id", None) + if tags_elem is not None: + tags = TagItem.from_xml_element(tags_elem, ns) + view_item.tags = tags + view_item._initial_tags = copy.copy(tags) + return view_item diff --git a/tableauserverclient/models/workbook_item.py b/tableauserverclient/models/workbook_item.py index debbf30b5..16e05498b 100644 --- a/tableauserverclient/models/workbook_item.py +++ b/tableauserverclient/models/workbook_item.py @@ -20,7 +20,7 @@ class WorkbookItem(object): - def __init__(self, project_id: str, name: Optional[str] = None, show_tabs: bool = False) -> None: + def __init__(self, project_id: Optional[str] = None, name: Optional[str] = None, show_tabs: bool = False) -> None: self._connections = None self._content_url = None self._webpage_url = None @@ -38,7 +38,8 @@ def __init__(self, project_id: str, name: Optional[str] = None, show_tabs: bool self.name = name self._description = None self.owner_id: Optional[str] = None - self.project_id = project_id + # workaround for Personal Space workbooks without a project + self.project_id: Optional[str] = project_id or uuid.uuid4().__str__() self.show_tabs = show_tabs self.hidden_views: Optional[List[str]] = None self.tags: Set[str] = set() @@ -293,49 +294,16 @@ def from_response(cls, resp: str, ns: Dict[str, str]) -> List["WorkbookItem"]: parsed_response = fromstring(resp) all_workbook_xml = parsed_response.findall(".//t:workbook", namespaces=ns) for workbook_xml in all_workbook_xml: - ( - id, - name, - content_url, - webpage_url, - created_at, - description, - updated_at, - size, - show_tabs, - project_id, - project_name, - owner_id, - tags, - views, - data_acceleration_config, - ) = cls._parse_element(workbook_xml, ns) - - # workaround for Personal Space workbooks which won't have a project - if not project_id: - project_id = uuid.uuid4() - - workbook_item = cls(project_id) - workbook_item._set_values( - id, - name, - content_url, - webpage_url, - created_at, - description, - updated_at, - size, - show_tabs, - None, - project_name, - owner_id, - tags, - views, - data_acceleration_config, - ) + workbook_item = cls.from_xml(workbook_xml, ns) all_workbook_items.append(workbook_item) return all_workbook_items + @classmethod + def from_xml(cls, workbook_xml, ns): + workbook_item = cls() + workbook_item._set_values(*cls._parse_element(workbook_xml, ns)) + return workbook_item + @staticmethod def _parse_element(workbook_xml, ns): id = workbook_xml.get("id", None) diff --git a/tableauserverclient/server/endpoint/favorites_endpoint.py b/tableauserverclient/server/endpoint/favorites_endpoint.py index 5105b3bf4..dcddca259 100644 --- a/tableauserverclient/server/endpoint/favorites_endpoint.py +++ b/tableauserverclient/server/endpoint/favorites_endpoint.py @@ -1,14 +1,22 @@ import logging from .endpoint import Endpoint, api -from tableauserverclient.server import RequestFactory -from tableauserverclient.models import FavoriteItem - -from typing import Optional, TYPE_CHECKING - -if TYPE_CHECKING: - from ...models import DatasourceItem, FlowItem, ProjectItem, UserItem, ViewItem, WorkbookItem - from ..request_options import RequestOptions +from requests import Response + +from tableauserverclient.server import RequestFactory, RequestOptions, Resource +from tableauserverclient.models import ( + DatasourceItem, + FavoriteItem, + FlowItem, + MetricItem, + ProjectItem, + UserItem, + ViewItem, + WorkbookItem, + TableauItem, +) + +from typing import Optional logger = logging.getLogger("tableau.endpoint.favorites") @@ -20,74 +28,112 @@ def baseurl(self) -> str: # Gets all favorites @api(version="2.5") - def get(self, user_item: "UserItem", req_options: Optional["RequestOptions"] = None) -> None: + def get(self, user_item: UserItem, req_options: Optional[RequestOptions] = None) -> None: logger.info("Querying all favorites for user {0}".format(user_item.name)) url = "{0}/{1}".format(self.baseurl, user_item.id) server_response = self.get_request(url, req_options) - user_item._favorites = FavoriteItem.from_response(server_response.content, self.parent_srv.namespace) + # ---------add to favorites + + @api(version="3.15") + def add_favorite(self, user_item: UserItem, content_type: str, item: TableauItem) -> "Response": + url = "{0}/{1}".format(self.baseurl, user_item.id) + add_req = RequestFactory.Favorite.add_request(item.id, content_type, item.name) + server_response = self.put_request(url, add_req) + logger.info("Favorited {0} for user (ID: {1})".format(item.name, user_item.id)) + return server_response + @api(version="2.0") - def add_favorite_workbook(self, user_item: "UserItem", workbook_item: "WorkbookItem") -> None: + def add_favorite_workbook(self, user_item: UserItem, workbook_item: WorkbookItem) -> None: url = "{0}/{1}".format(self.baseurl, user_item.id) add_req = RequestFactory.Favorite.add_workbook_req(workbook_item.id, workbook_item.name) server_response = self.put_request(url, add_req) logger.info("Favorited {0} for user (ID: {1})".format(workbook_item.name, user_item.id)) @api(version="2.0") - def add_favorite_view(self, user_item: "UserItem", view_item: "ViewItem") -> None: + def add_favorite_view(self, user_item: UserItem, view_item: ViewItem) -> None: url = "{0}/{1}".format(self.baseurl, user_item.id) add_req = RequestFactory.Favorite.add_view_req(view_item.id, view_item.name) server_response = self.put_request(url, add_req) logger.info("Favorited {0} for user (ID: {1})".format(view_item.name, user_item.id)) @api(version="2.3") - def add_favorite_datasource(self, user_item: "UserItem", datasource_item: "DatasourceItem") -> None: + def add_favorite_datasource(self, user_item: UserItem, datasource_item: DatasourceItem) -> None: url = "{0}/{1}".format(self.baseurl, user_item.id) add_req = RequestFactory.Favorite.add_datasource_req(datasource_item.id, datasource_item.name) server_response = self.put_request(url, add_req) logger.info("Favorited {0} for user (ID: {1})".format(datasource_item.name, user_item.id)) @api(version="3.1") - def add_favorite_project(self, user_item: "UserItem", project_item: "ProjectItem") -> None: + def add_favorite_project(self, user_item: UserItem, project_item: ProjectItem) -> None: url = "{0}/{1}".format(self.baseurl, user_item.id) add_req = RequestFactory.Favorite.add_project_req(project_item.id, project_item.name) server_response = self.put_request(url, add_req) logger.info("Favorited {0} for user (ID: {1})".format(project_item.name, user_item.id)) @api(version="3.3") - def add_favorite_flow(self, user_item: "UserItem", flow_item: "FlowItem") -> None: + def add_favorite_flow(self, user_item: UserItem, flow_item: FlowItem) -> None: url = "{0}/{1}".format(self.baseurl, user_item.id) add_req = RequestFactory.Favorite.add_flow_req(flow_item.id, flow_item.name) server_response = self.put_request(url, add_req) logger.info("Favorited {0} for user (ID: {1})".format(flow_item.name, user_item.id)) + @api(version="3.3") + def add_favorite_metric(self, user_item: UserItem, metric_item: MetricItem) -> None: + url = "{0}/{1}".format(self.baseurl, user_item.id) + add_req = RequestFactory.Favorite.add_request(metric_item.id, Resource.Metric, metric_item.name) + server_response = self.put_request(url, add_req) + logger.info("Favorited metric {0} for user (ID: {1})".format(metric_item.name, user_item.id)) + + # ------- delete from favorites + # Response: + """ + + + + + + """ + + @api(version="3.15") + def delete_favorite(self, user_item: UserItem, content_type: Resource, item: TableauItem) -> None: + url = "{0}/{1}/{2}/{3}".format(self.baseurl, user_item.id, content_type, item.id) + logger.info("Removing favorite {0}({1}) for user (ID: {2})".format(content_type, item.id, user_item.id)) + self.delete_request(url) + @api(version="2.0") - def delete_favorite_workbook(self, user_item: "UserItem", workbook_item: "WorkbookItem") -> None: + def delete_favorite_workbook(self, user_item: UserItem, workbook_item: WorkbookItem) -> None: url = "{0}/{1}/workbooks/{2}".format(self.baseurl, user_item.id, workbook_item.id) - logger.info("Removing favorite {0} for user (ID: {1})".format(workbook_item.id, user_item.id)) + logger.info("Removing favorite workbook {0} for user (ID: {1})".format(workbook_item.id, user_item.id)) self.delete_request(url) @api(version="2.0") - def delete_favorite_view(self, user_item: "UserItem", view_item: "ViewItem") -> None: + def delete_favorite_view(self, user_item: UserItem, view_item: ViewItem) -> None: url = "{0}/{1}/views/{2}".format(self.baseurl, user_item.id, view_item.id) - logger.info("Removing favorite {0} for user (ID: {1})".format(view_item.id, user_item.id)) + logger.info("Removing favorite view {0} for user (ID: {1})".format(view_item.id, user_item.id)) self.delete_request(url) @api(version="2.3") - def delete_favorite_datasource(self, user_item: "UserItem", datasource_item: "DatasourceItem") -> None: + def delete_favorite_datasource(self, user_item: UserItem, datasource_item: DatasourceItem) -> None: url = "{0}/{1}/datasources/{2}".format(self.baseurl, user_item.id, datasource_item.id) logger.info("Removing favorite {0} for user (ID: {1})".format(datasource_item.id, user_item.id)) self.delete_request(url) @api(version="3.1") - def delete_favorite_project(self, user_item: "UserItem", project_item: "ProjectItem") -> None: + def delete_favorite_project(self, user_item: UserItem, project_item: ProjectItem) -> None: url = "{0}/{1}/projects/{2}".format(self.baseurl, user_item.id, project_item.id) - logger.info("Removing favorite {0} for user (ID: {1})".format(project_item.id, user_item.id)) + logger.info("Removing favorite project {0} for user (ID: {1})".format(project_item.id, user_item.id)) self.delete_request(url) @api(version="3.3") - def delete_favorite_flow(self, user_item: "UserItem", flow_item: "FlowItem") -> None: - url = "{0}/{1}/projects/{2}".format(self.baseurl, user_item.id, flow_item.id) - logger.info("Removing favorite {0} for user (ID: {1})".format(flow_item.id, user_item.id)) + def delete_favorite_flow(self, user_item: UserItem, flow_item: FlowItem) -> None: + url = "{0}/{1}/flows/{2}".format(self.baseurl, user_item.id, flow_item.id) + logger.info("Removing favorite flow {0} for user (ID: {1})".format(flow_item.id, user_item.id)) + self.delete_request(url) + + @api(version="3.15") + def delete_favorite_metric(self, user_item: UserItem, metric_item: MetricItem) -> None: + url = "{0}/{1}/metrics/{2}".format(self.baseurl, user_item.id, metric_item.id) + logger.info("Removing favorite metric {0} for user (ID: {1})".format(metric_item.id, user_item.id)) self.delete_request(url) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 050874c91..4140794b4 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -261,12 +261,16 @@ def update_req(self, dqw_item): class FavoriteRequest(object): - def _add_to_req(self, id_: str, target_type: str, label: str) -> bytes: + def add_request(self, id_: Optional[str], target_type: str, label: Optional[str]) -> bytes: """ """ + if id_ is None: + raise ValueError("Cannot add item as favorite without ID") + if label is None: + label = target_type xml_request = ET.Element("tsRequest") favorite_element = ET.SubElement(xml_request, "favorite") target = ET.SubElement(favorite_element, target_type) @@ -280,35 +284,35 @@ def add_datasource_req(self, id_: Optional[str], name: Optional[str]) -> bytes: raise ValueError("id must exist to add to favorites") if name is None: raise ValueError("Name must exist to add to favorites.") - return self._add_to_req(id_, FavoriteItem.Type.Datasource, name) + return self.add_request(id_, Resource.Datasource, name) def add_flow_req(self, id_: Optional[str], name: Optional[str]) -> bytes: if id_ is None: raise ValueError("id must exist to add to favorites") if name is None: raise ValueError("Name must exist to add to favorites.") - return self._add_to_req(id_, FavoriteItem.Type.Flow, name) + return self.add_request(id_, Resource.Flow, name) def add_project_req(self, id_: Optional[str], name: Optional[str]) -> bytes: if id_ is None: raise ValueError("id must exist to add to favorites") if name is None: raise ValueError("Name must exist to add to favorites.") - return self._add_to_req(id_, FavoriteItem.Type.Project, name) + return self.add_request(id_, Resource.Project, name) def add_view_req(self, id_: Optional[str], name: Optional[str]) -> bytes: if id_ is None: raise ValueError("id must exist to add to favorites") if name is None: raise ValueError("Name must exist to add to favorites.") - return self._add_to_req(id_, FavoriteItem.Type.View, name) + return self.add_request(id_, Resource.View, name) def add_workbook_req(self, id_: Optional[str], name: Optional[str]) -> bytes: if id_ is None: raise ValueError("id must exist to add to favorites") if name is None: raise ValueError("Name must exist to add to favorites.") - return self._add_to_req(id_, FavoriteItem.Type.Workbook, name) + return self.add_request(id_, Resource.Workbook, name) class FileuploadRequest(object): @@ -485,7 +489,8 @@ def update_req(self, project_item: "ProjectItem") -> bytes: def create_req(self, project_item: "ProjectItem") -> bytes: xml_request = ET.Element("tsRequest") project_element = ET.SubElement(xml_request, "project") - project_element.attrib["name"] = project_item.name + if project_item.name: + project_element.attrib["name"] = project_item.name if project_item.description: project_element.attrib["description"] = project_item.description if project_item.content_permissions: diff --git a/test/test_favorites.py b/test/test_favorites.py index 9dcc3bb38..6f0be3b3c 100644 --- a/test/test_favorites.py +++ b/test/test_favorites.py @@ -37,6 +37,8 @@ def test_get(self) -> None: self.assertEqual(len(self.user.favorites["datasources"]), 1) workbook = self.user.favorites["workbooks"][0] + print("favorited: ") + print(workbook) view = self.user.favorites["views"][0] datasource = self.user.favorites["datasources"][0] project = self.user.favorites["projects"][0] diff --git a/test/test_project.py b/test/test_project.py index 3c75a0d3c..33d9c3865 100644 --- a/test/test_project.py +++ b/test/test_project.py @@ -155,7 +155,7 @@ def test_create(self) -> None: self.assertEqual("9a8f2265-70f3-4494-96c5-e5949d7a1120", new_project.parent_id) def test_create_missing_name(self) -> None: - self.assertRaises(ValueError, TSC.ProjectItem, "") + TSC.ProjectItem() def test_populate_permissions(self) -> None: with open(asset(POPULATE_PERMISSIONS_XML), "rb") as f: diff --git a/test/test_project_model.py b/test/test_project_model.py index a8b96dc4f..6ddaf8607 100644 --- a/test/test_project_model.py +++ b/test/test_project_model.py @@ -4,15 +4,11 @@ class ProjectModelTests(unittest.TestCase): - def test_invalid_name(self): - self.assertRaises(ValueError, TSC.ProjectItem, None) - self.assertRaises(ValueError, TSC.ProjectItem, "") + def test_nullable_name(self): + TSC.ProjectItem(None) + TSC.ProjectItem("") project = TSC.ProjectItem("proj") - with self.assertRaises(ValueError): - project.name = None - - with self.assertRaises(ValueError): - project.name = "" + project.name = None def test_invalid_content_permissions(self): project = TSC.ProjectItem("proj")