Skip to content
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
4 changes: 2 additions & 2 deletions tableauserverclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
25 changes: 25 additions & 0 deletions tableauserverclient/models/view_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man, this pattern is screaming for a decorator or something.

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
Expand Down
2 changes: 1 addition & 1 deletion tableauserverclient/server/__init__.py
Original file line number Diff line number Diff line change
@@ -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,\
Expand Down
39 changes: 39 additions & 0 deletions tableauserverclient/server/endpoint/views_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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."
Expand All @@ -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:
Expand Down
48 changes: 30 additions & 18 deletions tableauserverclient/server/request_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
25 changes: 25 additions & 0 deletions test/assets/populate_csv.csv
Original file line number Diff line number Diff line change
@@ -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"
Binary file added test/assets/populate_pdf.pdf
Binary file not shown.
31 changes: 31 additions & 0 deletions test/test_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down