8000 Add support for PDF and CSV Export (#236) · rmagier1/server-client-python@728643e · GitHub
[go: up one dir, main page]

Skip to content

Commit 728643e

Browse files
authored
Add support for PDF and CSV Export (tableau#236)
Added support for PDF export and CSV export for a view. These follow the pattern introduced by high resolution image and follow the fetcher method we use elsewhere. Updated tests, fixed misc test issues, and fixed some oddities in the old `ImageRequestOptions`
1 parent d703fa4 commit 728643e

File tree

8 files changed

+153
-21
lines changed

8 files changed

+153
-21
lines changed

tableauserverclient/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
GroupItem, PaginationItem, ProjectItem, ScheduleItem, \
44
SiteItem, TableauAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \
55
HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem
6-
from .server import RequestOptions, ImageRequestOptions, Filter, Sort, Server, ServerResponseError,\
7-
MissingRequiredFieldError, NotSignedInError, Pager
6+
from .server import RequestOptions, ImageRequestOptions, PDFRequestOptions, Filter, Sort, \
7+
Server, ServerResponseError, MissingRequiredFieldError, NotSignedInError, Pager
88

99
from ._version import get_versions
1010
__version__ = get_versions()['version']

tableauserverclient/models/view_item.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ def __init__(self):
1111
self._name = None
1212
self._owner_id = None
1313
self._preview_image = None
14+
self._pdf = None
15+
self._csv = None
1416
self._total_views = None
1517
self._workbook_id = None
1618
self.tags = set()
@@ -21,6 +23,12 @@ def _set_preview_image(self, preview_image):
2123
def _set_image(self, image):
2224
self._image = image
2325

26+
def _set_pdf(self, pdf):
27+
self._pdf = pdf
28+
29+
def _set_csv(self, csv):
30+
self._csv = csv
31+
2432
@property
2533
def content_url(self):
2634
return self._content_url
@@ -31,6 +39,9 @@ def id(self):
3139

3240
@property
3341
def image(self):
42+
if self._image is None:
43+
error = "View item must be populated with its png image first."
44+
raise UnpopulatedPropertyError(error)
3445
return self._image()
3546

3647
@property
@@ -48,6 +59,20 @@ def preview_image(self):
4859
raise UnpopulatedPropertyError(error)
4960
return self._preview_image()
5061

62+
@property
63+
def pdf(self):
64+
if self._pdf is None:
65+
error = "View item must be populated with its pdf first."
66+
raise UnpopulatedPropertyError(error)
67+
return self._pdf()
68+
69+
@property
70+
def csv(self):
71+
if self._csv is None:
72+
error = "View item must be populated with its csv first."
73+
raise UnpopulatedPropertyError(error)
74+
return self._csv()
75+
5176
@property
5277
def total_views(self):
5378
return self._total_views

tableauserverclient/server/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .request_factory import RequestFactory
2-
from .request_options import ImageRequestOptions, RequestOptions
2+
from .request_options import ImageRequestOptions, PDFRequestOptions, RequestOptions
33
from .filter import Filter
44
from .sort import Sort
55
from .. import ConnectionItem, DatasourceItem,\

tableauserverclient/server/endpoint/views_endpoint.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .. import RequestFactory, ViewItem, PaginationItem
55
from ...models.tag_item import TagItem
66
import logging
7+
from contextlib import closing
78

89
logger = logging.getLogger('tableau.endpoint.views')
910

@@ -50,6 +51,7 @@ def _get_preview_for_view(self, view_item):
5051
image = server_response.content
5152
return image
5253

54+
@api(version="2.5")
5355
def populate_image(self, view_item, req_options=None):
5456
if not view_item.id:
5557
error = "View item missing ID."
@@ -67,6 +69,43 @@ def _get_view_image(self, view_item, req_options):
6769
image = server_response.content
6870
return image
6971

72+
@api(version="2.7")
73+
def populate_pdf(self, view_item, req_options=None):
74+
if not view_item.id:
75+
error = "View item missing ID."
76+
raise MissingRequiredFieldError(error)
77+
78+
def pdf_fetcher():
79+
return self._get_view_pdf(view_item, req_options)
80+
81+
view_item._set_pdf(pdf_fetcher)
82+
logger.info("Populated pdf for view (ID: {0})".format(view_item.id))
83+
84+
def _get_view_pdf(self, view_item, req_options):
85+
url = "{0}/{1}/pdf".format(self.baseurl, view_item.id)
86+
server_response = self.get_request(url, req_options)
87+
pdf = server_response.content
88+
return pdf
89+
90+
@api(version="2.7")
91+
def populate_csv(self, view_item, req_options=None):
92+
if not view_item.id:
93+
error = "View item missing ID."
94+
raise MissingRequiredFieldError(error)
95+
96+
def csv_fetcher():
97+
return self._get_view_csv(view_item, req_options)
98+
99+
view_item._set_csv(csv_fetcher)
100+
logger.info("Populated csv for view (ID: {0})".format(view_item.id))
101+
102+
def _get_view_csv(self, view_item, req_options):
103+
url = "{0}/{1}/data".format(self.baseurl, view_item.id)
104+
105+
with closing(self.get_request(url, parameters={"stream": True})) as server_response:
106+
csv = server_response.iter_content(1024)
107+
return csv
108+
70109
# Update view. Currently only tags can be updated
71110
def update(self, view_item):
72111
if not view_item.id:

tableauserverclient/server/request_options.py

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -63,35 +63,47 @@ class Resolution:
6363
High = 'high'
6464

6565
def __init__(self, imageresolution=None):
66-
self.imageresolution = imageresolution
67-
68-
def image_resolution(self, imageresolution):
69-
self.imageresolution = imageresolution
70-
return self
66+
self.image_resolution = imageresolution
7167

7268
def apply_query_params(self, url):
7369
params = []
7470
if self.image_resolution:
75-
params.append('resolution={0}'.format(self.imageresolution))
71+
params.append('resolution={0}'.format(self.image_resolution))
7672

7773
return "{0}?{1}".format(url, '&'.join(params))
7874

7975

80-
class ImageRequestOptions(RequestOptionsBase):
76+
class PDFRequestOptions(RequestOptionsBase):
8177
# if 'high' isn't specified, the REST API endpoint returns an image with standard resolution
82-
class Resolution:
83-
High = 'high'
84-
85-
def __init__(self, imageresolution=None):
86-
self.imageresolution = imageresolution
87-
88-
def image_resolution(self, imageresolution):
89-
self.imageresolution = imageresolution
90-
return self
78+
class PageType:
79+
A3 = "a3"
80+
A4 = "a4"
81+
A5 = "a5"
82+
B4 = "b4"
83+
B5 = "b5"
84+
Executive = "executive"
85+
Folio = "folio"
86+
Ledger = "ledger"
87+
Legal = "legal"
88+
Letter = "letter"
89+
Note = "note"
90+
Quarto = "quarto"
91+
Tabloid = "tabloid"
92+
93+
class Orientation:
94+
Portrait = "portrait"
95+
Landscape = "landscape"
96+
97+
def __init__(self, page_type=None, orientation=None):
98+
self.page_type = page_type
99+
self.orientation = orientation
91100

92101
def apply_query_params(self, url):
93102
params = []
94-
if self.image_resolution:
95-
params.append('resolution={0}'.format(self.imageresolution))
103+
if self.page_type:
104+
params.append('type={0}'.format(self.page_type))
105+
106+
if self.orientation:
107+
params.append('orientation={0}'.format(self.orientation))
96108

97109
return "{0}?{1}".format(url, '&'.join(params))

test/assets/populate_csv.csv

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Measure Names,Region,Profit Ratio,Sales per Customer,Distinct count of Customer Name,Measure Values,Profit,Quantity,Sales
2+
Count of Customers,South,14.4%,$711.83,438,438,"$45,047","5,004","$311,784"
3+
Sales,South,14.4%,$711.83,438,"311,783.644","$45,047","5,004","$311,784"
4+
Quantity,South,14.4%,$711.83,438,"5,004","$45,047","5,004","$311,784"
5+
Sales per Customer,South,14.4%,$711.83,438,711.834803653,"$45,047","5,004","$311,784"
6+
Profit,South,14.4%,$711.83,438,"45,047.2231","$45,047","5,004","$311,784"
7+
Profit Ratio,South,14.4%,$711.83,438,0.144482316,"$45,047","5,004","$311,784"
8+
Count of Customers,Central,9.3%,$746.66,566,566,"$39,176","6,990","$422,611"
9+
Sales,Central,9.3%,$746.66,566,"422,610.558800001","$39,176","6,990","$422,611"
10+
Quantity,Central,9.3%,$746.66,566,"6,990","$39,176","6,990","$422,611"
11+
Sales per Customer,Central,9.3%,$746.66,566,746.661764664,"$39,176","6,990","$422,611"
12+
Profit,Central,9.3%,$746.66,566,"39,176.1836","$39,176","6,990","$422,611"
13+
Profit Ratio,Central,9.3%,$746.66,566,0.092700437,"$39,176","6,990","$422,611"
14+
Count of Customers,East,12.7%,$825.74,624,624,"$65,476","8,255","$515,262"
15+
Sales,East,12.7%,$825.74,624,"515,261.598000001","$65,476","8,255","$515,262"
16+
Quantity,East,12.7%,$825.74,624,"8,255","$65,476","8,255","$515,262"
17+
Sales per Customer,East,12.7%,$825.74,624,825.739740385,"$65,476","8,255","$515,262"
18+
Profit,East,12.7%,$825.74,624,"65,475.852700000","$65,476","8,255","$515,262"
19+
Profit Ratio,East,12.7%,$825.74,624,0.127073030,"$65,476","8,255","$515,262"
20+
Count of Customers,West,14.4%,$906.73,630,630,"$82,264","9,544","$571,239"
21+
Sales,West,14.4%,$906.73,630,"571,239.036500001","$82,264","9,544","$571,239"
22+
Quantity,West,14.4%,$906.73,630,"9,544","$82,264","9,544","$571,239"
23+
Sales per Customer,West,14.4%,$906.73,630,906.728629365,"$82,264","9,544","$571,239"
24+
Profit,West,14.4%,$906.73,630,"82,263.903800000","$82,264","9,544","$571,239"
25+
Profit Ratio,West,14.4%,$906.73,630,0.144009598,"$82,264","9,544","$571,239"

test/assets/populate_pdf.pdf

149 KB
Binary file not shown.

test/test_view.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
ADD_TAGS_XML = os.path.join(TEST_ASSET_DIR, 'view_add_tags.xml')
99
GET_XML = os.path.join(TEST_ASSET_DIR, 'view_get.xml')
1010
POPULATE_PREVIEW_IMAGE = os.path.join(TEST_ASSET_DIR, 'Sample View Image.png')
11+
POPULATE_PDF = os.path.join(TEST_ASSET_DIR, 'populate_pdf.pdf')
12+
POPULATE_CSV = os.path.join(TEST_ASSET_DIR, 'populate_csv.csv')
1113
UPDATE_XML = os.path.join(TEST_ASSET_DIR, 'workbook_update.xml')
1214

1315

1416
class ViewTests(unittest.TestCase):
1517
def setUp(self):
1618
self.server = TSC.Server('http://test')
19+
self.server.version = '2.7'
1720

1821
# Fake sign in
1922
self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67'
@@ -88,6 +91,34 @@ def test_populate_image_high_resolution(self):
8891
self.server.views.populate_image(single_view, req_option)
8992
self.assertEqual(response, single_view.image)
9093

94+
def test_populate_pdf(self):
95+
with open(POPULATE_PDF, 'rb') as f:
96+
response = f.read()
97+
with requests_mock.mock() as m:
98+
m.get(self.baseurl + '/d79634e1-6063-4ec9-95ff-50acbf609ff5/pdf?type=letter&orientation=portrait',
99+
content=response)
100+
single_view = TSC.ViewItem()
101+
single_view._id = 'd79634e1-6063-4ec9-95ff-50acbf609ff5'
102+
103+
size = TSC.PDFRequestOptions.PageType.Letter
104+
orientation = TSC.PDFRequestOptions.Orientation.Portrait
105+
req_option = TSC.PDFRequestOptions(size, orientation)
106+
107+
self.server.views.populate_pdf(single_view, req_option)
108+
self.assertEqual(response, single_view.pdf)
109+
110+
def test_populate_csv(self):
111+
with open(POPULATE_CSV, 'rb') as f:
112+
response = f.read()
113+
with requests_mock.mock() as m:
114+
m.get(self.baseurl + '/d79634e1-6063-4ec9-95ff-50acbf609ff5/data', content=response)
115+
single_view = TSC.ViewItem()
116+
single_view._id = 'd79634e1-6063-4ec9-95ff-50acbf609ff5'
117+
self.server.views.populate_csv(single_view)
118+
119+
csv_file = b"".join(single_view.csv)
120+
self.assertEqual(response, csv_file)
121+
91122
def test_populate_image_missing_id(self):
92123
single_view = TSC.ViewItem()
93124
single_view._id = None

0 commit comments

Comments
 (0)
0