8000 Merge pull request #311 from tableau/async-publishing · tableau/server-client-python@5e88a41 · GitHub
[go: up one dir, main page]

Skip to content

Commit 5e88a41

Browse files
authored
Merge pull request #311 from tableau/async-publishing
Added support for asynchronous publishing on workbook and datasource endpoints
2 parents 3fe62c9 + d5bd7c8 commit 5e88a41

File tree

8 files changed

+114
-22
lines changed

8 files changed

+114
-22
lines changed

samples/publish_workbook.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def main():
2929
parser.add_argument('--filepath', '-f', required=True, help='filepath to the workbook to publish')
3030
parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error',
3131
help='desired logging level (set to error by default)')
32+
parser.add_argument('--as-job', '-a', help='Publishing asynchronously', action='store_true')
3233

3334
args = parser.parse_args()
3435

@@ -53,8 +54,12 @@ def main():
5354
# Step 3: If default project is found, form a new workbook item and publish.
5455
if default_project is not None:
5556
new_workbook = TSC.WorkbookItem(default_project.id)
56-
new_workbook = server.workbooks.publish(new_workbook, args.filepath, overwrite_true)
57-
print("Workbook published. ID: {0}".format(new_workbook.id))
57+
if args.as_job:
58+
new_job = server.workbooks.publish(new_workbook, args.filepath, overwrite_true, as_job=args.as_job)
59+
print("Workbook published. JOB ID: {0}".format(new_job.id))
60+
else:
61+
new_workbook = server.workbooks.publish(new_workbook, args.filepath, overwrite_true, as_job=args.as_job)
62+
print("Workbook published. ID: {0}".format(new_workbook.id))
5863
else:
5964
error = "The default project could not be found."
6065
raise LookupError(error)

tableauserverclient/models/job_item.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import xml.etree.ElementTree as ET
22
from .target import Target
3+
from ..datetime_helpers import parse_datetime
34

45

56
class JobItem(object):
6-
def __init__(self, id_, job_type, created_at, started_at=None, completed_at=None, finish_code=0):
7+
def __init__(self, id_, job_type, progress, created_at, started_at=None, completed_at=None, finish_code=0):
78
self._id = id_
89
self._type = job_type
10+
self._progress = progress
911
self._created_at = created_at
1012
self._started_at = started_at
1113
self._completed_at = completed_at
@@ -19,6 +21,10 @@ def id(self):
1921
def type(self):
2022
return self._type
2123

24+
@property
25+
def progress(self):
26+
return self._progress
27+
2228
@property
2329
def created_at(self):
2430
return self._created_at
@@ -37,7 +43,7 @@ def finish_code(self):
3743

3844
def __repr__(self):
3945
return "<Job#{_id} {_type} created_at({_created_at}) started_at({_started_at}) completed_at({_completed_at})" \
40-
" finish_code({_finish_code})>".format(**self.__dict__)
46+
" progress ({_progress}) finish_code({_finish_code})>".format(**self.__dict__)
4147

4248
@classmethod
4349
def from_response(cls, xml, ns):
@@ -53,8 +59,9 @@ def from_response(cls, xml, ns):
5359
def _parse_element(cls, element, ns):
5460
id_ = element.get('id', None)
5561
type_ = element.get('type', None)
56-
created_at = element.get('createdAt', None)
57-
started_at = element.get('startedAt', None)
58-
completed_at = element.get('completedAt', None)
62+
progress = element.get('progress', None)
63+
created_at = parse_datetime(element.get('createdAt', None))
64+
started_at = parse_datetime(element.get('startedAt', None))
65+
completed_at = parse_datetime(element.get('completedAt', None))
5966
finish_code = element.get('finishCode', -1)
60-
return cls(id_, type_, created_at, started_at, completed_at, finish_code)
67+
return cls(id_, type_, progress, created_at, started_at, completed_at, finish_code)

tableauserverclient/server/endpoint/datasources_endpoint.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ def refresh(self, datasource_item):
151151

152152
# Publish datasource
153153
@api(version="2.0")
154-
def publish(self, datasource_item, file_path, mode, connection_credentials=None):
154+
@parameter_added_in(as_job='3.0')
155+
def publish(self, datasource_item, file_path, mode, connection_credentials=None, as_job=False):
155156
if not os.path.isfile(file_path):
156157
error = "File path does not lead to an existing file."
157158
raise IOError(error)
@@ -174,6 +175,9 @@ def publish(self, datasource_item, file_path, mode, connection_credentials=None)
174175
if mode == self.parent_srv.PublishMode.Overwrite or mode == self.parent_srv.PublishMode.Append:
175176
url += '&{0}=true'.format(mode.lower())
176177

178+
if as_job:
179+
url += '&{0}=true'.format('asJob')
180+
177181
# Determine if chunking is required (64MB is the limit for single upload method)
178182
if os.path.getsize(file_path) >= FILESIZE_LIMIT:
179183
logger.info('Publishing {0} to server with chunking method (datasource over 64MB)'.format(filename))
@@ -190,6 +194,12 @@ def publish(self, datasource_item, file_path, mode, connection_credentials=None)
190194
file_contents,
191195
connection_credentials)
192196
server_response = self.post_request(url, xml_request, content_type)
193-
new_datasource = DatasourceItem.from_response(server_response.content, self.parent_srv.namespace)[0]
194-
logger.info('Published {0} (ID: {1})'.format(filename, new_datasource.id))
195-
return new_datasource
197+
198+
if as_job:
199+
new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
200+
logger.info('Published {0} (JOB_ID: {1}'.format(filename, new_job.id))
201+
return new_job
202+
else:
203+
new_datasource = DatasourceItem.from_response(server_response.content, self.parent_srv.namespace)[0]
204+
logger.info('Published {0} (ID: {1})'.format(filename, new_datasource.id))
205+
return new_datasource

tableauserverclient/server/endpoint/workbooks_endpoint.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ def _get_wb_preview_image(self, workbook_item):
199199

200200
# Publishes workbook. Chunking method if file over 64MB
201201
@api(version="2.0")
202-
def publish(self, workbook_item, file_path, mode, connection_credentials=None):
202+
@parameter_added_in(as_job='3.0')
203+
def publish(self, workbook_item, file_path, mode, connection_credentials=None, as_job=False):
203204
if not os.path.isfile(file_path):
204205
error = "File path does not lead to an existing file."
205206
raise IOError(error)
@@ -225,6 +226,9 @@ def publish(self, workbook_item, file_path, mode, connection_credentials=None):
225226
error = 'Workbooks cannot be appended.'
226227
raise ValueError(error)
227228

229+
if as_job:
230+
url += '&{0}=true'.format('asJob')
231+
228232
# Determine if chunking is required (64MB is the limit for single upload method)
229233
if os.path.getsize(file_path) >= FILESIZE_LIMIT:
230234
logger.info('Publishing {0} to server with chunking method (workbook over 64MB)'.format(filename))
@@ -241,6 +245,11 @@ def publish(self, workbook_item, file_path, mode, connection_credentials=None):
241245
file_contents,
242246
connection_credentials)
243247
server_response = self.post_request(url, xml_request, content_type)
244-
new_workbook = WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)[0]
245-
logger.info('Published {0} (ID: {1})'.format(filename, new_workbook.id))
246-
return new_workbook
248+
if as_job:
249+
new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
250+
logger.info('Published {0} (JOB_ID: {1}'.format(filename, new_job.id))
251+
return new_job
252+
else:
253+
new_workbook = WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)[0]
254+
logger.info('Published {0} (ID: {1})'.format(filename, new_workbook.id))
255+
return new_workbook
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<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-3.0.xsd">
3+
<job id="9a373058-af5f-4f83-8662-98b3e0228a73" mode="Asynchronous" type="PublishDatasource" progress="0" createdAt="2018-06-30T00:54:54Z" finishCode="1"/>
4+
</tsResponse>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<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-3.0.xsd">
3+
<job id="7c3d599e-949f-44c3-94a1-f30ba85757e4" mode="Asynchronous" type="PublishWorkbook" progress="0" createdAt="2018-06-29T23:22:32Z" finishCode="1"/>
4+
</tsResponse>

test/test_datasource.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
GET_BY_ID_XML = 'datasource_get_by_id.xml'
1212
POPULATE_CONNECTIONS_XML = 'datasource_populate_connections.xml'
1313
PUBLISH_XML = 'datasource_publish.xml'
14+
PUBLISH_XML_ASYNC = 'datasource_publish_async.xml'
1415
UPDATE_XML = 'datasource_update.xml'
1516
UPDATE_CONNECTION_XML = 'datasource_connection_update.xml'
1617

@@ -172,9 +173,11 @@ def test_publish(self):
172173
with requests_mock.mock() as m:
173174
m.post(self.baseurl, text=response_xml)
174175
new_datasource = TSC.DatasourceItem('SampleDS', 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
176+
publish_mode = self.server.PublishMode.CreateNew
177+
175178
new_datasource = self.server.datasources.publish(new_datasource,
176179
asset('SampleDS.tds'),
177-
mode=self.server.PublishMode.CreateNew)
180+
mode=publish_mode)
178181

179182
self.assertEqual('e76a1461-3b1d-4588-bf1b-17551a879ad9', new_datasource.id)
180183
self.assertEqual('SampleDS', new_datasource.name)
@@ -186,6 +189,24 @@ def test_publish(self):
186189
self.assertEqual('default', new_datasource.project_name)
187190
self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', new_datasource.owner_id)
188191

192+
def test_publish_async(self):
193+
response_xml = read_xml_asset(PUBLISH_XML_ASYNC)
194+
with requests_mock.mock() as m:
195+
m.post(self.baseurl, text=response_xml)
196+
new_datasource = TSC.DatasourceItem('SampleDS', 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
197+
publish_mode = self.server.PublishMode.CreateNew
198+
199+
new_job = self.server.datasources.publish(new_datasource,
200+
asset('SampleDS.tds'),
201+
mode=publish_mode,
202+
as_job=True)
203+
204+
self.assertEqual('9a373058-af5f-4f83-8662-98b3e0228a73', new_job.id)
205+
self.assertEqual('PublishDatasource', new_job.type)
206+
self.assertEqual('0', new_job.progress)
207+
self.assertEqual('2018-06-30T00:54:54Z', format_datetime(new_job.created_at))
208+
self.assertEqual('1', new_job.finish_code)
209+
189210
def test_delete(self):
190211
with requests_mock.mock() as m:
191212
m.delete(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb', status_code=204)

test/test_workbook.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
POPULATE_VIEWS_XML = os.path.join(TEST_ASSET_DIR, 'workbook_populate_views.xml')
1616
POPULATE_VIEWS_USAGE_XML = os.path.join(TEST_ASSET_DIR, 'workbook_populate_views_usage.xml')
1717
PUBLISH_XML = os.path.join(TEST_ASSET_DIR, 'workbook_publish.xml')
18+
PUBLISH_ASYNC_XML = os.path.join(TEST_ASSET_DIR, 'workbook_publish_async.xml')
1819
UPDATE_XML = os.path.join(TEST_ASSET_DIR, 'workbook_update.xml')
1920

2021

@@ -285,10 +286,17 @@ def test_publish(self):
285286
response_xml = f.read().decode('utf-8')
286287
with requests_mock.mock() as m:
287288
m.post(self.baseurl, text=response_xml)
288-
new_workbook = TSC.WorkbookItem(name='Sample', show_tabs=False,
289+
290+
new_workbook = TSC.WorkbookItem(name='Sample',
291+
show_tabs=False,
289292
project_id='ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
290-
new_workbook = self.server.workbooks.publish(new_workbook, os.path.join(TEST_ASSET_DIR, 'SampleWB.twbx'),
291-
self.server.PublishMode.CreateNew)
293+
294+
sample_workbok = os.path.join(TEST_ASSET_DIR, 'SampleWB.twbx')
295+
publish_mode = self.server.PublishMode.CreateNew
296+
297+
new_workbook = self.server.workbooks.publish(new_workbook,
298+
sample_workbok,
299+
publish_mode)
292300

293301
self.assertEqual('a8076ca1-e9d8-495e-bae6-c684dbb55836', new_workbook.id)
294302
self.assertEqual('RESTAPISample', new_workbook.name)
@@ -304,10 +312,34 @@ def test_publish(self):
304312
self.assertEqual('GDP per capita', new_workbook.views[0].name)
305313
self.assertEqual('RESTAPISample_0/sheets/GDPpercapita', new_workbook.views[0].content_url)
306314

315+
def test_publish_async(self):
316+
with open(PUBLISH_ASYNC_XML, 'rb') as f:
317+
response_xml = f.read().decode('utf-8')
318+
with requests_mock.mock() as m:
319+
m.post(self.baseurl, text=response_xml)
320+
321+
new_workbook = TSC.WorkbookItem(name='Sample',
322+
show_tabs=False,
323+
project_id='ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
324+
325+
sample_workbok = os.path.join(TEST_ASSET_DIR, 'SampleWB.twbx')
326+
publish_mode = self.server.PublishMode.CreateNew
327+
328+
new_job = self.server.workbooks.publish(new_workbook,
329+
sample_workbok,
330+
publish_mode,
331+
as_job=True)
332+
333+
self.assertEqual('7c3d599e-949f-44c3-94a1-f30ba85757e4', new_job.id)
334+
self.assertEqual('PublishWorkbook', new_job.type)
335+
self.assertEqual('0', new_job.progress)
336+
self.assertEqual('2018-06-29T23:22:32Z', format_datetime(new_job.created_at))
337+
self.assertEqual('1', new_job.finish_code)
338+
307339
def test_publish_invalid_file(self):
308340
new_workbook = TSC.WorkbookItem('test', 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
309-
self.assertRaises(IOError, self.server.workbooks.publish, new_workbook,
310-
'.', self.server.PublishMode.CreateNew)
341+
self.assertRaises(IOError, self.server.workbooks.publish, new_workbook, '.',
342+
self.server.PublishMode.CreateNew)
311343

312344
def test_publish_invalid_file_type(self):
313345
new_workbook = TSC.WorkbookItem('test', 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')

0 commit comments

Comments
 (0)
0