diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py
index b5b50fe59..c86057a3d 100644
--- a/tableauserverclient/models/__init__.py
+++ b/tableauserverclient/models/__init__.py
@@ -5,6 +5,7 @@
from .datasource_item import DatasourceItem
from .database_item import DatabaseItem
from .exceptions import UnpopulatedPropertyError
+from .favorites_item import FavoriteItem
from .group_item import GroupItem
from .flow_item import FlowItem
from .interval_item import IntervalItem, DailyInterval, WeeklyInterval, MonthlyInterval, HourlyInterval
diff --git a/tableauserverclient/models/favorites_item.py b/tableauserverclient/models/favorites_item.py
new file mode 100644
index 000000000..7d2408f93
--- /dev/null
+++ b/tableauserverclient/models/favorites_item.py
@@ -0,0 +1,49 @@
+import xml.etree.ElementTree as ET
+import logging
+from .workbook_item import WorkbookItem
+from .view_item import ViewItem
+from .project_item import ProjectItem
+from .datasource_item import DatasourceItem
+
+logger = logging.getLogger('tableau.models.favorites_item')
+
+
+class FavoriteItem:
+ class Type:
+ Workbook = 'workbook'
+ Datasource = 'datasource'
+ View = 'view'
+ Project = 'project'
+
+ @classmethod
+ def from_response(cls, xml, namespace):
+ favorites = {
+ 'datasources': [],
+ 'projects': [],
+ 'views': [],
+ 'workbooks': [],
+ }
+
+ parsed_response = ET.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))
+ if 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)
+
+ return favorites
diff --git a/tableauserverclient/models/user_item.py b/tableauserverclient/models/user_item.py
index 3df2004bf..9be38210f 100644
--- a/tableauserverclient/models/user_item.py
+++ b/tableauserverclient/models/user_item.py
@@ -42,6 +42,7 @@ def __init__(self, name=None, site_role=None, auth_setting=None):
self._id = None
self._last_login = None
self._workbooks = None
+ self._favorites = None
self.email = None
self.fullname = None
self.name = name
@@ -99,6 +100,13 @@ def workbooks(self):
raise UnpopulatedPropertyError(error)
return self._workbooks()
+ @property
+ def favorites(self):
+ if self._favorites is None:
+ error = "User item must be populated with favorites first."
+ raise UnpopulatedPropertyError(error)
+ return self._favorites
+
def to_reference(self):
return ResourceReference(id_=self.id, tag_name=self.tag_name)
diff --git a/tableauserverclient/server/__init__.py b/tableauserverclient/server/__init__.py
index f382d0dba..aff549559 100644
--- a/tableauserverclient/server/__init__.py
+++ b/tableauserverclient/server/__init__.py
@@ -8,7 +8,7 @@
PermissionsRule, Permission, ColumnItem, FlowItem, WebhookItem
from .endpoint import Auth, Datasources, Endpoint, Groups, Projects, Schedules, \
Sites, Tables, Users, Views, Workbooks, Subscriptions, ServerResponseError, \
- MissingRequiredFieldError, Flows
+ MissingRequiredFieldError, Flows, Favorites
from .server import Server
from .pager import Pager
from .exceptions import NotSignedInError
diff --git a/tableauserverclient/server/endpoint/__init__.py b/tableauserverclient/server/endpoint/__init__.py
index fce86f98d..1341ecd3f 100644
--- a/tableauserverclient/server/endpoint/__init__.py
+++ b/tableauserverclient/server/endpoint/__init__.py
@@ -3,6 +3,7 @@
from .datasources_endpoint import Datasources
from .databases_endpoint import Databases
from .endpoint import Endpoint
+from .favorites_endpoint import Favorites
from .flows_endpoint import Flows
from .exceptions import ServerResponseError, MissingRequiredFieldError, ServerInfoEndpointNotFoundError
from .groups_endpoint import Groups
diff --git a/tableauserverclient/server/endpoint/favorites_endpoint.py b/tableauserverclient/server/endpoint/favorites_endpoint.py
new file mode 100644
index 000000000..61536ce42
--- /dev/null
+++ b/tableauserverclient/server/endpoint/favorites_endpoint.py
@@ -0,0 +1,78 @@
+from .endpoint import Endpoint, api
+from .exceptions import MissingRequiredFieldError
+from .. import RequestFactory
+from ...models import FavoriteItem
+from ..pager import Pager
+import xml.etree.ElementTree as ET
+import logging
+import copy
+
+logger = logging.getLogger('tableau.endpoint.favorites')
+
+
+class Favorites(Endpoint):
+ @property
+ def baseurl(self):
+ return "{0}/sites/{1}/favorites".format(self.parent_srv.baseurl, self.parent_srv.site_id)
+
+ # Gets all favorites
+ @api(version="2.5")
+ def get(self, user_item, req_options=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)
+
+ @api(version="2.0")
+ def add_favorite_workbook(self, user_item, workbook_item):
+ 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, view_item):
+ 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, datasource_item):
+ 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, project_item):
+ 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="2.0")
+ def delete_favorite_workbook(self, user_item, workbook_item):
+ 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))
+ self.delete_request(url)
+
+ @api(version="2.0")
+ def delete_favorite_view(self, user_item, view_item):
+ 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))
+ self.delete_request(url)
+
+ @api(version="2.3")
+ def delete_favorite_datasource(self, user_item, datasource_item):
+ 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, project_item):
+ 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))
+ self.delete_request(url)
diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py
index 4307e6496..9c869c686 100644
--- a/tableauserverclient/server/request_factory.py
+++ b/tableauserverclient/server/request_factory.py
@@ -3,7 +3,7 @@
from requests.packages.urllib3.fields import RequestField
from requests.packages.urllib3.filepost import encode_multipart_formdata
-from ..models import TaskItem
+from ..models import TaskItem, UserItem, GroupItem, PermissionsRule, FavoriteItem
def _add_multipart(parts):
@@ -149,6 +149,34 @@ def publish_req_chunked(self, datasource_item, connection_credentials=None, conn
return _add_multipart(parts)
+class FavoriteRequest(object):
+ def _add_to_req(self, id_, target_type, label):
+ '''
+
+
+
+ '''
+ xml_request = ET.Element('tsRequest')
+ favorite_element = ET.SubElement(xml_request, 'favorite')
+ target = ET.SubElement(favorite_element, target_type)
+ favorite_element.attrib['label'] = label
+ target.attrib['id'] = id_
+
+ return ET.tostring(xml_request)
+
+ def add_datasource_req(self, id_, name):
+ return self._add_to_req(id_, FavoriteItem.Type.Datasource, name)
+
+ def add_project_req(self, id_, name):
+ return self._add_to_req(id_, FavoriteItem.Type.Project, name)
+
+ def add_view_req(self, id_, name):
+ return self._add_to_req(id_, FavoriteItem.Type.View, name)
+
+ def add_workbook_req(self, id_, name):
+ return self._add_to_req(id_, FavoriteItem.Type.Workbook, name)
+
+
class FileuploadRequest(object):
def chunk_req(self, chunk):
parts = {'request_payload': ('', '', 'text/xml'),
@@ -605,6 +633,7 @@ class RequestFactory(object):
Datasource = DatasourceRequest()
Database = DatabaseRequest()
Empty = EmptyRequest()
+ Favorite = FavoriteRequest()
Fileupload = FileuploadRequest()
Flow = FlowRequest()
Group = GroupRequest()
diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py
index 37371d707..c36ee0f4b 100644
--- a/tableauserverclient/server/server.py
+++ b/tableauserverclient/server/server.py
@@ -4,7 +4,7 @@
from ..namespace import Namespace
from .endpoint import Sites, Views, Users, Groups, Workbooks, Datasources, Projects, Auth, \
Schedules, ServerInfo, Tasks, Subscriptions, Jobs, Metadata,\
- Databases, Tables, Flows, Webhooks, DataAccelerationReport
+ Databases, Tables, Flows, Webhooks, DataAccelerationReport, Favorites
from .endpoint.exceptions import EndpointUnavailableError, ServerInfoEndpointNotFoundError
import requests
@@ -46,6 +46,7 @@ def __init__(self, server_address, use_server_version=False):
self.jobs = Jobs(self)
self.workbooks = Workbooks(self)
self.datasources = Datasources(self)
+ self.favorites = Favorites(self)
self.flows = Flows(self)
self.projects = Projects(self)
self.schedules = Schedules(self)
diff --git a/test/assets/favorites_add_datasource.xml b/test/assets/favorites_add_datasource.xml
new file mode 100644
index 000000000..a1f47ab4f
--- /dev/null
+++ b/test/assets/favorites_add_datasource.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/favorites_add_project.xml b/test/assets/favorites_add_project.xml
new file mode 100644
index 000000000..699e6a4cd
--- /dev/null
+++ b/test/assets/favorites_add_project.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/favorites_add_view.xml b/test/assets/favorites_add_view.xml
new file mode 100644
index 000000000..f6fc15c9a
--- /dev/null
+++ b/test/assets/favorites_add_view.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/favorites_add_workbook.xml b/test/assets/favorites_add_workbook.xml
new file mode 100644
index 000000000..c8008c9b8
--- /dev/null
+++ b/test/assets/favorites_add_workbook.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/favorites_get.xml b/test/assets/favorites_get.xml
new file mode 100644
index 000000000..3d2e2ee6a
--- /dev/null
+++ b/test/assets/favorites_get.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/test_favorites.py b/test/test_favorites.py
new file mode 100644
index 000000000..f76517b64
--- /dev/null
+++ b/test/test_favorites.py
@@ -0,0 +1,129 @@
+import unittest
+import os
+import requests_mock
+import xml.etree.ElementTree as ET
+import tableauserverclient as TSC
+from tableauserverclient.datetime_helpers import format_datetime
+from tableauserverclient.server.endpoint.exceptions import InternalServerError
+from tableauserverclient.server.request_factory import RequestFactory
+from ._utils import read_xml_asset, read_xml_assets, asset
+
+GET_FAVORITES_XML = 'favorites_get.xml'
+ADD_FAVORITE_WORKBOOK_XML = 'favorites_add_workbook.xml'
+ADD_FAVORITE_VIEW_XML = 'favorites_add_view.xml'
+ADD_FAVORITE_DATASOURCE_XML = 'favorites_add_datasource.xml'
+ADD_FAVORITE_PROJECT_XML = 'favorites_add_project.xml'
+
+
+class FavoritesTests(unittest.TestCase):
+ def setUp(self):
+ self.server = TSC.Server('http://test')
+ self.server.version = '2.5'
+
+ # Fake signin
+ self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67'
+ self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM'
+
+ self.baseurl = self.server.favorites.baseurl
+ self.user = TSC.UserItem('alice', TSC.UserItem.Roles.Viewer)
+ self.user._id = 'dd2239f6-ddf1-4107-981a-4cf94e415794'
+
+ def test_get(self):
+ response_xml = read_xml_asset(GET_FAVORITES_XML)
+ with requests_mock.mock() as m:
+ m.get('{0}/{1}'.format(self.baseurl, self.user.id),
+ text=response_xml)
+ self.server.favorites.get(self.user)
+ self.assertIsNotNone(self.user._favorites)
+ self.assertEqual(len(self.user.favorites['workbooks']), 1)
+ self.assertEqual(len(self.user.favorites['views']), 1)
+ self.assertEqual(len(self.user.favorites['projects']), 1)
+ self.assertEqual(len(self.user.favorites['datasources']), 1)
+
+ workbook = self.user.favorites['workbooks'][0]
+ view = self.user.favorites['views'][0]
+ datasource = self.user.favorites['datasources'][0]
+ project = self.user.favorites['projects'][0]
+
+ self.assertEqual(workbook.id, '6d13b0ca-043d-4d42-8c9d-3f3313ea3a00')
+ self.assertEqual(view.id, 'd79634e1-6063-4ec9-95ff-50acbf609ff5')
+ self.assertEqual(datasource.id, 'e76a1461-3b1d-4588-bf1b-17551a879ad9')
+ self.assertEqual(project.id, '1d0304cd-3796-429f-b815-7258370b9b74')
+
+ def test_add_favorite_workbook(self):
+ response_xml = read_xml_asset(ADD_FAVORITE_WORKBOOK_XML)
+ workbook = TSC.WorkbookItem('')
+ workbook._id = '6d13b0ca-043d-4d42-8c9d-3f3313ea3a00'
+ workbook.name = 'Superstore'
+ with requests_mock.mock() as m:
+ m.put('{0}/{1}'.format(self.baseurl, self.user.id),
+ text=response_xml)
+ self.server.favorites.add_favorite_workbook(self.user, workbook)
+
+ def test_add_favorite_view(self):
+ response_xml = read_xml_asset(ADD_FAVORITE_VIEW_XML)
+ view = TSC.ViewItem()
+ view._id = 'd79634e1-6063-4ec9-95ff-50acbf609ff5'
+ view._name = 'ENDANGERED SAFARI'
+ with requests_mock.mock() as m:
+ m.put('{0}/{1}'.format(self.baseurl, self.user.id),
+ text=response_xml)
+ self.server.favorites.add_favorite_view(self.user, view)
+
+ def test_add_favorite_datasource(self):
+ response_xml = read_xml_asset(ADD_FAVORITE_DATASOURCE_XML)
+ datasource = TSC.DatasourceItem('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
+ datasource._id = 'e76a1461-3b1d-4588-bf1b-17551a879ad9'
+ datasource.name = 'SampleDS'
+ with requests_mock.mock() as m:
+ m.put('{0}/{1}'.format(self.baseurl, self.user.id),
+ text=response_xml)
+ self.server.favorites.add_favorite_datasource(self.user, datasource)
+
+ def test_add_favorite_project(self):
+ self.server.version = '3.1'
+ baseurl = self.server.favorites.baseurl
+ response_xml = read_xml_asset(ADD_FAVORITE_PROJECT_XML)
+ project = TSC.ProjectItem('Tableau')
+ project._id = '1d0304cd-3796-429f-b815-7258370b9b74'
+ with requests_mock.mock() as m:
+ m.put('{0}/{1}'.format(baseurl, self.user.id),
+ text=response_xml)
+ self.server.favorites.add_favorite_project(self.user, project)
+
+ def test_delete_favorite_workbook(self):
+ workbook = TSC.WorkbookItem('')
+ workbook._id = '6d13b0ca-043d-4d42-8c9d-3f3313ea3a00'
+ workbook.name = 'Superstore'
+ with requests_mock.mock() as m:
+ m.delete('{0}/{1}/workbooks/{2}'.format(self.baseurl, self.user.id,
+ workbook.id))
+ self.server.favorites.delete_favorite_workbook(self.user, workbook)
+
+ def test_delete_favorite_view(self):
+ view = TSC.ViewItem()
+ view._id = 'd79634e1-6063-4ec9-95ff-50acbf609ff5'
+ view._name = 'ENDANGERED SAFARI'
+ with requests_mock.mock() as m:
+ m.delete('{0}/{1}/views/{2}'.format(self.baseurl, self.user.id,
+ view.id))
+ self.server.favorites.delete_favorite_view(self.user, view)
+
+ def test_delete_favorite_datasource(self):
+ datasource = TSC.DatasourceItem('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
+ datasource._id = 'e76a1461-3b1d-4588-bf1b-17551a879ad9'
+ datasource.name = 'SampleDS'
+ with requests_mock.mock() as m:
+ m.delete('{0}/{1}/datasources/{2}'.format(self.baseurl, self.user.id,
+ datasource.id))
+ self.server.favorites.delete_favorite_datasource(self.user, datasource)
+
+ def test_delete_favorite_project(self):
+ self.server.version = '3.1'
+ baseurl = self.server.favorites.baseurl
+ project = TSC.ProjectItem('Tableau')
+ project._id = '1d0304cd-3796-429f-b815-7258370b9b74'
+ with requests_mock.mock() as m:
+ m.delete('{0}/{1}/projects/{2}'.format(baseurl, self.user.id,
+ project.id))
+ self.server.favorites.delete_favorite_project(self.user, project)
diff --git a/test/test_project.py b/test/test_project.py
index b57d52df5..5e9869c6e 100644
--- a/test/test_project.py
+++ b/test/test_project.py
@@ -100,14 +100,14 @@ def test_update_datasource_default_permission(self):
new_rules = self.server.projects.update_datasource_default_permissions(project, rules)
- self.assertEquals('b4488bce-80f0-11ea-af1c-976d0c1dab39', new_rules[0].grantee.id)
+ self.assertEqual('b4488bce-80f0-11ea-af1c-976d0c1dab39', new_rules[0].grantee.id)
updated_capabilities = new_rules[0].capabilities
- self.assertEquals(4, len(updated_capabilities))
- self.assertEquals('Deny', updated_capabilities['ExportXml'])
- self.assertEquals('Allow', updated_capabilities['Read'])
- self.assertEquals('Allow', updated_capabilities['Write'])
- self.assertEquals('Allow', updated_capabilities['Connect'])
+ self.assertEqual(4, len(updated_capabilities))
+ self.assertEqual('Deny', updated_capabilities['ExportXml'])
+ self.assertEqual('Allow', updated_capabilities['Read'])
+ self.assertEqual('Allow', updated_capabilities['Write'])
+ self.assertEqual('Allow', updated_capabilities['Connect'])
def test_update_missing_id(self):
single_project = TSC.ProjectItem('test')