8000 205 - Pager/Fetcher all the things! by t8y8 · Pull Request #231 · tableau/server-client-python · GitHub
[go: up one dir, main page]

Skip to content

205 - Pager/Fetcher all the things! #231

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

Merged
merged 10 commits into from
Sep 26, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tableauserverclient/models/datasource_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def connections(self):
if self._connections is None:
error = 'Datasource item must be populated with connections first.'
raise UnpopulatedPropertyError(error)
return self._connections
return self._connections()

@property
def content_url(self):
Expand Down
1 change: 0 additions & 1 deletion tableauserverclient/models/schedule_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ def updated_at(self):

def _parse_common_tags(self, schedule_xml, ns):
if not isinstance(schedule_xml, ET.Element):
print(ns)
schedule_xml = ET.fromstring(schedule_xml).find('.//t:schedule', namespaces=ns)
if schedule_xml is not None:
(_, name, _, _, updated_at, _, next_run_at, end_schedule_at, execution_order,
Expand Down
2 changes: 1 addition & 1 deletion tableauserverclient/models/user_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def workbooks(self):
if self._workbooks is None:
error = "User item must be populated with workbooks first."
raise UnpopulatedPropertyError(error)
return self._workbooks
return self._workbooks()

def _set_workbooks(self, workbooks):
self._workbooks = workbooks
Expand Down
10 changes: 8 additions & 2 deletions tableauserverclient/models/view_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ def __init__(self):
self._workbook_id = None
self.tags = set()

def _set_preview_image(self, preview_image):
self._preview_image = preview_image

def _set_image(self, image):
self._image = image

@property
def content_url(self):
return self._content_url
Expand All @@ -25,7 +31,7 @@ def id(self):

@property
def image(self):
return self._image
return self._image()

@property
def name(self):
Expand All @@ -40,7 +46,7 @@ def preview_image(self):
if self._preview_image is None:
error = "View item must be populated with its preview image first."
raise UnpopulatedPropertyError(error)
return self._preview_image
return self._preview_image()

@property
def total_views(self):
Expand Down
16 changes: 13 additions & 3 deletions tableauserverclient/models/workbook_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def connections(self):
if self._connections is None:
error = "Workbook item must be populated with connections first."
raise UnpopulatedPropertyError(error)
return self._connections
return self._connections()

@property
def content_url(self):
Expand All @@ -49,7 +49,7 @@ def preview_image(self):
if self._preview_image is None:
error = "Workbook item must be populated with its preview image first."
raise UnpopulatedPropertyError(error)
return self._preview_image
return self._preview_image()

@property
def project_id(self):
Expand Down Expand Up @@ -83,10 +83,20 @@ def updated_at(self):

@property
def views(self):
# Views can be set in an initial workbook response OR by a call
# to Server. Without getting too fancy, I think we can rely on
# returning a list from the response, until they call
# populate_workbook, in which case we bind the fetcher and
# return a callable.
if self._views is None:
error = "Workbook item must be populated with views first."
raise UnpopulatedPropertyError(error)
return self._views
elif callable(self._views):
# We've called `populate_views` on this model
return self._views()
else:
# We had views included in a WorkbookItem response
return self._views

def _set_connections(self, connections):
self._connections = connections
Expand Down
15 changes: 11 additions & 4 deletions tableauserverclient/server/endpoint/datasources_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,19 @@ def populate_connections(self, datasource_item):
if not datasource_item.id:
error = 'Datasource item missing ID. Datasource must be retrieved from server first.'
raise MissingRequiredFieldError(error)
url = '{0}/{1}/connections'.format(self.baseurl, datasource_item.id)
server_response = self.get_request(url)
datasource_item._set_connections(
ConnectionItem.from_response(server_response.content, self.parent_srv.namespace))

def connections_fetcher():
return self._get_datasource_connections(datasource_item)

datasource_item._set_connections(connections_fetcher)
logger.info('Populated connections for datasource (ID: {0})'.format(datasource_item.id))

def _get_datasource_connections(self, datasource_item, req_options=None):
url = '{0}/{1}/connections'.format(self.baseurl, datasource_item.id)
server_response = self.get_request(url, req_options)
connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
return connections

# Delete 1 datasource by id
@api(version="2.0")
def delete(self, datasource_id):
Expand Down
3 changes: 1 addition & 2 deletions tableauserverclient/server/endpoint/groups_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .exceptions import MissingRequiredFieldError
from ...models.exceptions import UnpopulatedPropertyError
from .. import RequestFactory, GroupItem, UserItem, PaginationItem
from ..pager import Pager
import logging

logger = logging.getLogger('tableau.endpoint.groups')
Expand All @@ -25,8 +26,6 @@ def get(self, req_options=None):
# Gets all users in a given group
@api(version="2.0")
def populate_users(self, group_item, req_options=None):
from .. import Pager

if not group_item.id:
error = "Group item missing ID. Group must be retrieved from server first."
raise MissingRequiredFieldError(error)
Expand Down
12 changes: 10 additions & 2 deletions tableauserverclient/server/endpoint/users_endpoint.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .endpoint import Endpoint, api
from .exceptions import MissingRequiredFieldError
from .. import RequestFactory, UserItem, WorkbookItem, PaginationItem
from ..pager import Pager
import logging
import copy

Expand Down Expand Up @@ -73,12 +74,19 @@ def populate_workbooks(self, user_item, req_options=None):
if not user_item.id:
error = "User item missing ID."
raise MissingRequiredFieldError(error)

def wb_pager():
return Pager(lambda options: self._get_wbs_for_user(user_item, options), req_options)

user_item._set_workbooks(wb_pager)

def _get_wbs_for_user(self, user_item, req_options=None):
url = "{0}/{1}/workbooks".format(self.baseurl, user_item.id)
server_response = self.get_request(url, req_options)
logger.info('Populated workbooks for user (ID: {0})'.format(user_item.id))
user_item._set_workbooks(WorkbookItem.from_response(server_response.content, self.parent_srv.namespace))
workbook_item = WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
return pagination_item
return workbook_item, pagination_item

def populate_favorites(self, user_item):
raise NotImplementedError('REST API currently does not support the ability to query favorites')
24 changes: 20 additions & 4 deletions tableauserverclient/server/endpoint/views_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,37 @@ def populate_preview_image(self, view_item):
if not view_item.id or not view_item.workbook_id:
error = "View item missing ID or workbook ID."
raise MissingRequiredFieldError(error)

def image_fetcher():
return self._get_preview_for_view(view_item)

view_item._set_preview_image(image_fetcher)
logger.info('Populated preview image for view (ID: {0})'.format(view_item.id))

def _get_preview_for_view(self, view_item):
url = "{0}/workbooks/{1}/views/{2}/previewImage".format(self.siteurl,
view_item.workbook_id,
view_item.id)
server_response = self.get_request(url)
view_item._preview_image = server_response.content
logger.info('Populated preview image for view (ID: {0})'.format(view_item.id))
image = server_response.content
return image

def populate_image(self, view_item, req_options=None):
if not view_item.id:
error = "View item missing ID."
raise MissingRequiredFieldError(error)

def image_fetcher():
return self._get_view_image(view_item, req_options)

view_item._set_image(image_fetcher)
logger.info("Populated image for view (ID: {0})".format(view_item.id))

def _get_view_image(self, view_item, req_options):
url = "{0}/{1}/image".format(self.baseurl, view_item.id)
server_response = self.get_request(url, req_options)
view_item._image = server_response.content
logger.info("Populated image for view (ID: {0})".format(view_item.id))
image = server_response.content
return image

# Update view. Currently only tags can be updated
def update(self, view_item):
Expand Down
42 changes: 33 additions & 9 deletions tableauserverclient/server/endpoint/workbooks_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,34 +122,58 @@ def populate_views(self, workbook_item):
if not workbook_item.id:
error = "Workbook item missing ID. Workbook must be retrieved from server first."
raise MissingRequiredFieldError(error)

def view_fetcher():
return self._get_views_for_workbook(workbook_item)

workbook_item._set_views(view_fetcher)
logger.info('Populated views for workbook (ID: {0}'.format(workbook_item.id))

def _get_views_for_workbook(self, workbook_item):
url = "{0}/{1}/views".format(self.baseurl, workbook_item.id)
server_response = self.get_request(url)
workbook_item._set_views(ViewItem.from_response(server_response.content,
self.parent_srv.namespace,
workbook_id=workbook_item.id))
logger.info('Populated views for workbook (ID: {0}'.format(workbook_item.id))
views = ViewItem.from_response(server_response.content,
self.parent_srv.namespace,
workbook_id=workbook_item.id)
return views

# Get all connections of workbook
@api(version="2.0")
def populate_connections(self, workbook_item):
if not workbook_item.id:
error = "Workbook item missing ID. Workbook must be retrieved from server first."
raise MissingRequiredFieldError(error)
url = "{0}/{1}/connections".format(self.baseurl, workbook_item.id)
server_response = self.get_request(url)
workbook_item._set_connections(ConnectionItem.from_response(server_response.content, self.parent_srv.namespace))

def connection_fetcher():
return self._get_workbook_connections(workbook_item)

workbook_item._set_connections(connection_fetcher)
logger.info('Populated connections for workbook (ID: {0})'.format(workbook_item.id))

def _get_workbook_connections(self, workbook_item, req_options=None):
url = "{0}/{1}/connections".format(self.baseurl, workbook_item.id)
server_response = self.get_request(url, req_options)
connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
return connections

# Get preview image of workbook
@api(version="2.0")
def populate_preview_image(self, workbook_item):
if not workbook_item.id:
error = "Workbook item missing ID. Workbook must be retrieved from server first."
raise MissingRequiredFieldError(error)

def image_fetcher():
return self._get_wb_preview_image(workbook_item)

workbook_item._set_preview_image(image_fetcher)
logger.info('Populated preview image for workbook (ID: {0})'.format(workbook_item.id))

def _get_wb_preview_image(self, workbook_item):
url = "{0}/{1}/previewImage".format(self.baseurl, workbook_item.id)
server_response = self.get_request(url)
workbook_item._set_preview_image(server_response.content)
logger.info('Populated preview image for workbook (ID: {0})'.format(workbook_item.id))
preview_image = server_response.content
return preview_image

# Publishes workbook. Chunking method if file over 64MB
@api(version="2.0")
Expand Down
8 changes: 8 additions & 0 deletions test/assets/datasource_populate_connections.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version='1.0' encoding='UTF-8'?>
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.8.xsd">
<connections>
<connection id="be786ae0-d2bf-4a4b-9b34-e2de8d2d4488" type="textscan" serverAddress="" userName="" />
<connection id="970e24bc-e200-4841-a3e9-66e7d122d77e" type="sqlserver" serverAddress="database.com" userName="heero" />
<connection id="7d85b889-283b-42df-b23e-3c811e402f1f" type="textscan" serverAddress="forty-two.net" userName="duo" />
</connections>
</tsResponse>
20 changes: 20 additions & 0 deletions test/test_datasource.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
GET_XML = os.path.join(TEST_ASSET_DIR, 'datasource_get.xml')
GET_EMPTY_XML = os.path.join(TEST_ASSET_DIR, 'datasource_get_empty.xml')
GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, 'datasource_get_by_id.xml')
POPULATE_CONNECTIONS_XML = os.path.join(TEST_ASSET_DIR, 'datasource_populate_connections.xml')
PUBLISH_XML = os.path.join(TEST_ASSET_DIR, 'datasource_publish.xml')
UPDATE_XML = os.path.join(TEST_ASSET_DIR, 'datasource_update.xml')

Expand Down Expand Up @@ -135,6 +136,25 @@ def test_update_tags(self):
self.assertEqual(single_datasource.tags, updated_datasource.tags)
self.assertEqual(single_datasource._initial_tags, updated_datasource._initial_tags)

def test_populate_connections(self):
with open(POPULATE_CONNECTIONS_XML, 'rb') as f:
response_xml = f.read().decode('utf-8')
with requests_mock.mock() as m:
m.get(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections', text=response_xml)
single_datasource = TSC.DatasourceItem('test', '1d0304cd-3796-429f-b815-7258370b9b74')
single_datasource.owner_id = 'dd2239f6-ddf1-4107-981a-4cf94e415794'
single_datasource._id = '9dbd2263-16b5-46e1-9c43-a76bb8ab65fb'
self.server.datasources.populate_connections(single_datasource)

self.assertEqual('9dbd2263-16b5-46e1-9c43-a76bb8ab65fb', single_datasource.id)

connections = single_datasource.connections
self.assertTrue(connections)
ds1, ds2, ds3 = connections
self.assertEqual(ds1.id, 'be786ae0-d2bf-4a4b-9b34-e2de8d2d4488')
self.assertEqual(ds2.id, '970e24bc-e200-4841-a3e9-66e7d122d77e')
self.assertEqual(ds3.id, '7d85b889-283b-42df-b23e-3c811e402f1f')

def test_publish(self):
with open(PUBLISH_XML, 'rb') as f:
response_xml = f.read().decode('utf-8')
Expand Down
4 changes: 2 additions & 2 deletions test/test_server_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_server_info_use_highest_version_downgrades(self):
# Return a 404 for serverInfo so we can pretend this is an old Server
m.get(self.server.server_address + "/api/2.4/serverInfo", text=si_response_xml, status_code=404)
m.get(self.server.server_address + "/auth?format=xml", text=auth_response_xml)
self.server.use_highest_version()
self.server.use_server_version()
self.assertEqual(self.server.version, '2.2')

def test_server_info_use_highest_version_upgrades(self):
Expand All @@ -49,7 +49,7 @@ def test_server_info_use_highest_version_upgrades(self):
m.get(self.server.server_address + "/api/2.4/serverInfo", text=si_response_xml)
# Pretend we're old
self.server.version = '2.0'
self.server.use_highest_version()
self.server.use_server_version()
# Did we upgrade to 2.4?
self.assertEqual(self.server.version, '2.4')

Expand Down
29 changes: 14 additions & 15 deletions test/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,21 +128,20 @@ def test_populate_workbooks(self):
text=response_xml)
single_user = TSC.UserItem('test', 'Interactor')
single_user._id = 'dd2239f6-ddf1-4107-981a-4cf94e415794'
pagination_item = self.server.users.populate_workbooks(single_user)

workbook_list = single_user.workbooks
self.assertEqual(1, pagination_item.total_available)
self.assertEqual('3cc6cd06-89ce-4fdc-b935-5294135d6d42', workbook_list[0].id)
self.assertEqual('SafariSample', workbook_list[0].name)
self.assertEqual('SafariSample', workbook_list[0].content_url)
self.assertEqual(False, workbook_list[0].show_tabs)
self.assertEqual(26, workbook_list[0].size)
self.assertEqual('2016-07-26T20:34:56Z', format_datetime(workbook_list[0].created_at))
self.assertEqual('2016-07-26T20:35:05Z', format_datetime(workbook_list[0].updated_at))
self.assertEqual('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', workbook_list[0].project_id)
self.assertEqual('default', workbook_list[0].project_name)
self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', workbook_list[0].owner_id)
self.assertEqual(set(['Safari', 'Sample']), workbook_list[0].tags)
self.server.users.populate_workbooks(single_user)

workbook_list = list(single_user.workbooks)
self.assertEqual('3cc6cd06-89ce-4fdc-b935-5294135d6d42', workbook_list[0].id)
self.assertEqual('SafariSample', workbook_list[0].name)
self.assertEqual('SafariSample', workbook_list[0].content_url)
self.assertEqual(False, workbook_list[0].show_tabs)
self.assertEqual(26, workbook_list[0].size)
self.assertEqual('2016-07-26T20:34:56Z', format_datetime(workbook_list[0].created_at))
self.assertEqual('2016-07-26T20:35:05Z', format_datetime(workbook_list[0].updated_at))
self.assertEqual('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', workbook_list[0].project_id)
self.assertEqual('default', workbook_list[0].project_name)
self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', workbook_list[0].owner_id)
self.assertEqual(set(['Safari', 'Sample']), workbook_list[0].tags)

def test_populate_workbooks_missing_id(self):
single_user = TSC.UserItem('test', 'Interactor')
Expand Down
Loading
0