diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py index 440d99aaf..677625526 100644 --- a/tableauserverclient/__init__.py +++ b/tableauserverclient/__init__.py @@ -3,8 +3,8 @@ GroupItem, PaginationItem, ProjectItem, ScheduleItem, \ SiteItem, TableauAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \ HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem -from .server import RequestOptions, ImageRequestOptions, Filter, Sort, Server, ServerResponseError,\ - MissingRequiredFieldError, NotSignedInError, Pager +from .server import RequestOptions, ImageRequestOptions, PDFRequestOptions, Filter, Sort, \ + Server, ServerResponseError, MissingRequiredFieldError, NotSignedInError, Pager from ._version import get_versions __version__ = get_versions()['version'] diff --git a/tableauserverclient/models/view_item.py b/tableauserverclient/models/view_item.py index 80de88352..4dfcf41c1 100644 --- a/tableauserverclient/models/view_item.py +++ b/tableauserverclient/models/view_item.py @@ -11,6 +11,8 @@ def __init__(self): self._name = None self._owner_id = None self._preview_image = None + self._pdf = None + self._csv = None self._total_views = None self._workbook_id = None self.tags = set() @@ -21,6 +23,12 @@ def _set_preview_image(self, preview_image): def _set_image(self, image): self._image = image + def _set_pdf(self, pdf): + self._pdf = pdf + + def _set_csv(self, csv): + self._csv = csv + @property def content_url(self): return self._content_url @@ -31,6 +39,9 @@ def id(self): @property def image(self): + if self._image is None: + error = "View item must be populated with its png image first." + raise UnpopulatedPropertyError(error) return self._image() @property @@ -48,6 +59,20 @@ def preview_image(self): raise UnpopulatedPropertyError(error) return self._preview_image() + @property + def pdf(self): + if self._pdf is None: + error = "View item must be populated with its pdf first." + raise UnpopulatedPropertyError(error) + return self._pdf() + + @property + def csv(self): + if self._csv is None: + error = "View item must be populated with its csv first." + raise UnpopulatedPropertyError(error) + return self._csv() + @property def total_views(self): return self._total_views diff --git a/tableauserverclient/server/__init__.py b/tableauserverclient/server/__init__.py index 01a044137..5168f1b92 100644 --- a/tableauserverclient/server/__init__.py +++ b/tableauserverclient/server/__init__.py @@ -1,5 +1,5 @@ from .request_factory import RequestFactory -from .request_options import ImageRequestOptions, RequestOptions +from .request_options import ImageRequestOptions, PDFRequestOptions, RequestOptions from .filter import Filter from .sort import Sort from .. import ConnectionItem, DatasourceItem,\ diff --git a/tableauserverclient/server/endpoint/views_endpoint.py b/tableauserverclient/server/endpoint/views_endpoint.py index 43349aec0..892abd8ce 100644 --- a/tableauserverclient/server/endpoint/views_endpoint.py +++ b/tableauserverclient/server/endpoint/views_endpoint.py @@ -4,6 +4,7 @@ from .. import RequestFactory, ViewItem, PaginationItem from ...models.tag_item import TagItem import logging +from contextlib import closing logger = logging.getLogger('tableau.endpoint.views') @@ -50,6 +51,7 @@ def _get_preview_for_view(self, view_item): image = server_response.content return image + @api(version="2.5") def populate_image(self, view_item, req_options=None): if not view_item.id: error = "View item missing ID." @@ -67,6 +69,43 @@ def _get_view_image(self, view_item, req_options): image = server_response.content return image + @api(version="2.7") + def populate_pdf(self, view_item, req_options=None): + if not view_item.id: + error = "View item missing ID." + raise MissingRequiredFieldError(error) + + def pdf_fetcher(): + return self._get_view_pdf(view_item, req_options) + + view_item._set_pdf(pdf_fetcher) + logger.info("Populated pdf for view (ID: {0})".format(view_item.id)) + + def _get_view_pdf(self, view_item, req_options): + url = "{0}/{1}/pdf".format(self.baseurl, view_item.id) + server_response = self.get_request(url, req_options) + pdf = server_response.content + return pdf + + @api(version="2.7") + def populate_csv(self, view_item, req_options=None): + if not view_item.id: + error = "View item missing ID." + raise MissingRequiredFieldError(error) + + def csv_fetcher(): + return self._get_view_csv(view_item, req_options) + + view_item._set_csv(csv_fetcher) + logger.info("Populated csv for view (ID: {0})".format(view_item.id)) + + def _get_view_csv(self, view_item, req_options): + url = "{0}/{1}/data".format(self.baseurl, view_item.id) + + with closing(self.get_request(url, parameters={"stream": True})) as server_response: + csv = server_response.iter_content(1024) + return csv + # Update view. Currently only tags can be updated def update(self, view_item): if not view_item.id: diff --git a/tableauserverclient/server/request_options.py b/tableauserverclient/server/request_options.py index acd82c68e..009daa6f1 100644 --- a/tableauserverclient/server/request_options.py +++ b/tableauserverclient/server/request_options.py @@ -63,35 +63,47 @@ class Resolution: High = 'high' def __init__(self, imageresolution=None): - self.imageresolution = imageresolution - - def image_resolution(self, imageresolution): - self.imageresolution = imageresolution - return self + self.image_resolution = imageresolution def apply_query_params(self, url): params = [] if self.image_resolution: - params.append('resolution={0}'.format(self.imageresolution)) + params.append('resolution={0}'.format(self.image_resolution)) return "{0}?{1}".format(url, '&'.join(params)) -class ImageRequestOptions(RequestOptionsBase): +class PDFRequestOptions(RequestOptionsBase): # if 'high' isn't specified, the REST API endpoint returns an image with standard resolution - class Resolution: - High = 'high' - - def __init__(self, imageresolution=None): - self.imageresolution = imageresolution - - def image_resolution(self, imageresolution): - self.imageresolution = imageresolution - return self + class PageType: + A3 = "a3" + A4 = "a4" + A5 = "a5" + B4 = "b4" + B5 = "b5" + Executive = "executive" + Folio = "folio" + Ledger = "ledger" + Legal = "legal" + Letter = "letter" + Note = "note" + Quarto = "quarto" + Tabloid = "tabloid" + + class Orientation: + Portrait = "portrait" + Landscape = "landscape" + + def __init__(self, page_type=None, orientation=None): + self.page_type = page_type + self.orientation = orientation def apply_query_params(self, url): params = [] - if self.image_resolution: - params.append('resolution={0}'.format(self.imageresolution)) + if self.page_type: + params.append('type={0}'.format(self.page_type)) + + if self.orientation: + params.append('orientation={0}'.format(self.orientation)) return "{0}?{1}".format(url, '&'.join(params)) diff --git a/test/assets/populate_csv.csv b/test/assets/populate_csv.csv new file mode 100644 index 000000000..b12de4304 --- /dev/null +++ b/test/assets/populate_csv.csv @@ -0,0 +1,25 @@ +Measure Names,Region,Profit Ratio,Sales per Customer,Distinct count of Customer Name,Measure Values,Profit,Quantity,Sales +Count of Customers,South,14.4%,$711.83,438,438,"$45,047","5,004","$311,784" +Sales,South,14.4%,$711.83,438,"311,783.644","$45,047","5,004","$311,784" +Quantity,South,14.4%,$711.83,438,"5,004","$45,047","5,004","$311,784" +Sales per Customer,South,14.4%,$711.83,438,711.834803653,"$45,047","5,004","$311,784" +Profit,South,14.4%,$711.83,438,"45,047.2231","$45,047","5,004","$311,784" +Profit Ratio,South,14.4%,$711.83,438,0.144482316,"$45,047","5,004","$311,784" +Count of Customers,Central,9.3%,$746.66,566,566,"$39,176","6,990","$422,611" +Sales,Central,9.3%,$746.66,566,"422,610.558800001","$39,176","6,990","$422,611" +Quantity,Central,9.3%,$746.66,566,"6,990","$39,176","6,990","$422,611" +Sales per Customer,Central,9.3%,$746.66,566,746.661764664,"$39,176","6,990","$422,611" +Profit,Central,9.3%,$746.66,566,"39,176.1836","$39,176","6,990","$422,611" +Profit Ratio,Central,9.3%,$746.66,566,0.092700437,"$39,176","6,990","$422,611" +Count of Customers,East,12.7%,$825.74,624,624,"$65,476","8,255","$515,262" +Sales,East,12.7%,$825.74,624,"515,261.598000001","$65,476","8,255","$515,262" +Quantity,East,12.7%,$825.74,624,"8,255","$65,476","8,255","$515,262" +Sales per Customer,East,12.7%,$825.74,624,825.739740385,"$65,476","8,255","$515,262" +Profit,East,12.7%,$825.74,624,"65,475.852700000","$65,476","8,255","$515,262" +Profit Ratio,East,12.7%,$825.74,624,0.127073030,"$65,476","8,255","$515,262" +Count of Customers,West,14.4%,$906.73,630,630,"$82,264","9,544","$571,239" +Sales,West,14.4%,$906.73,630,"571,239.036500001","$82,264","9,544","$571,239" +Quantity,West,14.4%,$906.73,630,"9,544","$82,264","9,544","$571,239" +Sales per Customer,West,14.4%,$906.73,630,906.728629365,"$82,264","9,544","$571,239" +Profit,West,14.4%,$906.73,630,"82,263.903800000","$82,264","9,544","$571,239" +Profit Ratio,West,14.4%,$906.73,630,0.144009598,"$82,264","9,544","$571,239" diff --git a/test/assets/populate_pdf.pdf b/test/assets/populate_pdf.pdf new file mode 100644 index 000000000..4d3319442 Binary files /dev/null and b/test/assets/populate_pdf.pdf differ diff --git a/test/test_view.py b/test/test_view.py index 54972745a..7a35b3754 100644 --- a/test/test_view.py +++ b/test/test_view.py @@ -8,12 +8,15 @@ ADD_TAGS_XML = os.path.join(TEST_ASSET_DIR, 'view_add_tags.xml') GET_XML = os.path.join(TEST_ASSET_DIR, 'view_get.xml') POPULATE_PREVIEW_IMAGE = os.path.join(TEST_ASSET_DIR, 'Sample View Image.png') +POPULATE_PDF = os.path.join(TEST_ASSET_DIR, 'populate_pdf.pdf') +POPULATE_CSV = os.path.join(TEST_ASSET_DIR, 'populate_csv.csv') UPDATE_XML = os.path.join(TEST_ASSET_DIR, 'workbook_update.xml') class ViewTests(unittest.TestCase): def setUp(self): self.server = TSC.Server('http://test') + self.server.version = '2.7' # Fake sign in self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67' @@ -88,6 +91,34 @@ def test_populate_image_high_resolution(self): self.server.views.populate_image(single_view, req_option) self.assertEqual(response, single_view.image) + def test_populate_pdf(self): + with open(POPULATE_PDF, 'rb') as f: + response = f.read() + with requests_mock.mock() as m: + m.get(self.baseurl + '/d79634e1-6063-4ec9-95ff-50acbf609ff5/pdf?type=letter&orientation=portrait', + content=response) + single_view = TSC.ViewItem() + single_view._id = 'd79634e1-6063-4ec9-95ff-50acbf609ff5' + + size = TSC.PDFRequestOptions.PageType.Letter + orientation = TSC.PDFRequestOptions.Orientation.Portrait + req_option = TSC.PDFRequestOptions(size, orientation) + + self.server.views.populate_pdf(single_view, req_option) + self.assertEqual(response, single_view.pdf) + + def test_populate_csv(self): + with open(POPULATE_CSV, 'rb') as f: + response = f.read() + with requests_mock.mock() as m: + m.get(self.baseurl + '/d79634e1-6063-4ec9-95ff-50acbf609ff5/data', content=response) + single_view = TSC.ViewItem() + single_view._id = 'd79634e1-6063-4ec9-95ff-50acbf609ff5' + self.server.views.populate_csv(single_view) + + csv_file = b"".join(single_view.csv) + self.assertEqual(response, csv_file) + def test_populate_image_missing_id(self): single_view = TSC.ViewItem() single_view._id = None