8000 Merge multicredential and as job by graysonarts · Pull Request #313 · tableau/server-client-python · GitHub
[go: up one dir, main page]

Skip to content

Merge multicredential and as job #313

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 26 commits into from
Jul 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
608aa76
Add parent_id descr (#266)
sotnich Feb 21, 2018
3fe62c9
Adding all of the new site roles (#291)
May 8, 2018
a6e1631
Added asynchronous publish support for workbook and datasource endpoint
Jun 30, 2018
ef71db0
Reverting a typo
gaoang2148 Jun 30, 2018
8cc3032
Fixing unittests
gaoang2148 Jun 30, 2018
f5f57f4
Fixed indentation
gaoang2148 Jun 30, 2018
1fcdc12
Addressing comment
gaoang2148 Jul 2, 2018
d5bd7c8
Make it shiny! ... and fix failed test ;)
gaoang2148 Jul 4, 2018
5e88a41
Merge pull request #311 from tableau/async-publishing
gaoang2148 Jul 5, 2018
433974d
Implement view filters on the populate* request options (#260)
Jan 31, 2018
d1c3b42
Export sample (#263)
Feb 16, 2018
9716cff
Change to correct parent project id tag name (#267)
sotnich Feb 21, 2018
de977bb
add export_wb sample for 'fullpdf' (#264)
Feb 21, 2018
3e5329f
Add a simple static method to strip non-XML responses from debug log …
t8y8 Mar 9, 2018
9301ef2
Fix update datasource connection server port (#283)
shinchris Apr 6, 2018
3ab46fb
Add ability to rename workbook using the 'update workbook' endpoint (…
shinchris Apr 10, 2018
0c4984f
adding project id field to view_item (#285)
shinchris Apr 11, 2018
54f0b15
adding more fields for filtering ability (#286)
shinchris Apr 16, 2018
dc81b4a
Refactor the refresh sample to be more explicit (#288)
Apr 17, 2018
9fc2bb4
277 update group feature (#279)
sotnich Apr 19, 2018
cbbf7ff
Multi-Credential Support in TSC (#276)
t8y8 Apr 20, 2018
dfbf48b
fixes issue #296 (#297)
anipmehta May 30, 2018
b0faef9
initial checkin for get background jobs (#298)
May 31, 2018
5ddd547
Add cancel job (#299)
Jun 1, 2018
2e1284b
Prep v0.7 (#310)
Jul 6, 2018
1aff17a
Merge branch 'development' into merge_multicredential_and_as_job
Jul 6, 2018
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
3 changes: 2 additions & 1 deletion docs/docs/api-ref.md
Original file line number Diff line number Diff line change
Expand Up @@ -1077,7 +1077,7 @@ The project resources for Tableau are defined in the `ProjectItem` class. The cl

```py

ProjectItem(name, description=None, content_permissions=None)
ProjectItem(name, description=None, content_permissions=None, parent_id=None)

```
The project resources for Tableau are defined in the `ProjectItem` class. The class corresponds to the project resources you can access using the Tableau Server REST API.
Expand All @@ -1090,6 +1090,7 @@ Name | Description
`name` | Name of the project.
`description` | The description of the project.
`id` | The project id.
`parent_id` | The parent project id.



Expand Down
14 changes: 9 additions & 5 deletions samples/publish_workbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def main():
parser.add_argument('--filepath', '-f', required=True, help='filepath to the workbook to publish')
parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error',
help='desired logging level (set to error by default)')
parser.add_argument('--as-job', '-a', help='Publishing asynchronously', action='store_true')

args = parser.parse_args()

Expand Down Expand Up @@ -67,11 +68,14 @@ def main():
# Step 3: If default project is found, form a new workbook item and publish.
if default_project is not None:
new_workbook = TSC.WorkbookItem(default_project.id)
new_workbook = server.workbooks.publish(new_workbook,
args.filepath,
overwrite_true,
connections=all_connections)
print("Workbook published. ID: {0}".format(new_workbook.id))
if args.as_job:
new_job = server.workbooks.publish(new_workbook, args.filepath, overwrite_true,
connections=all_connections, as_job=args.as_job)
print("Workbook published. JOB ID: {0}".format(new_job.id))
else:
new_workbook = server.workbooks.publish(new_workbook, args.filepath, overwrite_true,
connections=all_connections, as_job=args.as_job)
print("Workbook published. ID: {0}".format(new_workbook.id))
else:
error = "The default project could not be found."
raise LookupError(error)
Expand Down
19 changes: 13 additions & 6 deletions tableauserverclient/models/job_item.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import xml.etree.ElementTree as ET
from ..datetime_helpers import parse_datetime
from .target import Target
from ..datetime_helpers import parse_datetime


class JobItem(object):
def __init__(self, id_, job_type, created_at, started_at=None, completed_at=None, finish_code=0):
def __init__(self, id_, job_type, progress, created_at, started_at=None, completed_at=None, finish_code=0):
self._id = id_
self._type = job_type
self._progress = progress
self._created_at = created_at
self._started_at = started_at
self._completed_at = completed_at
Expand All @@ -20,6 +22,10 @@ def id(self):
def type(self):
return self._type

@property
def progress(self):
return self._progress

@property
def created_at(self):
return self._created_at
Expand All @@ -38,7 +44,7 @@ def finish_code(self):

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

@classmethod
def from_response(cls, xml, ns):
Expand All @@ -54,11 +60,12 @@ def from_response(cls, xml, ns):
def _parse_element(cls, element, ns):
id_ = element.get('id', None)
type_ = element.get('type', None)
created_at = element.get('createdAt', None)
started_at = element.get('startedAt', None)
completed_at = element.get('completedAt', None)
progress = element.get('progress', None)
created_at = parse_datetime(element.get('createdAt', None))
started_at = parse_datetime(element.get('startedAt', None))
completed_at = parse_datetime(element.get('completedAt', None))
finish_code = element.get('finishCode', -1)
return cls(id_, type_, created_at, started_at, completed_at, finish_code)
return cls(id_, type_, progress, created_at, started_at, completed_at, finish_code)


class BackgroundJobItem(object):
Expand Down
11 changes: 11 additions & 0 deletions tableauserverclient/models/user_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ class Roles:
ViewerWithPublish = 'ViewerWithPublish'
Guest = 'Guest'

Creator = 'Creator'
Explorer = 'Explorer'
ExplorerCanPublish = 'ExplorerCanPublish'
ReadOnly = 'ReadOnly'
SiteAdministratorCreator = 'SiteAdministratorCreator'
SiteAdministratorExplorer = 'SiteAdministratorExplorer'
UnlicensedWithPublish = 'UnlicensedWithPublish'

class Auth:
SAML = 'SAML'
ServerDefault = 'ServerDefault'
Expand Down Expand Up @@ -147,3 +155,6 @@ def _parse_element(user_xml, ns):
domain_name = domain_elem.get('name', None)

return id, name, site_role, last_login, external_auth_user_id, fullname, email, auth_setting, domain_name

def __repr__(self):
return "<User {} name={} role={}>".format(self.id, self.name, self.site_role)
20 changes: 15 additions & 5 deletions tableauserverclient/server/endpoint/datasources_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,9 @@ def refresh(self, datasource_item):

# Publish datasource
@api(version="2.0")
@parameter_added_in(connections="99.99")
def publish(self, datasource_item, file_path, mode, connection_credentials=None, connections=None):
@parameter_added_in(connections="2.8")
@parameter_added_in(as_job='3.0')
def publish(self, datasource_item, file_path, mode, connection_credentials=None, connections=None, as_job=False):
if not os.path.isfile(file_path):
error = "File path does not lead to an existing file."
raise IOError(error)
Expand All @@ -175,6 +176,9 @@ def publish(self, datasource_item, file_path, mode, connection_credentials=None,
if mode == self.parent_srv.PublishMode.Overwrite or mode == self.parent_srv.PublishMode.Append:
url += '&{0}=true'.format(mode.lower())

if as_job:
url += '&{0}=true'.format('asJob')

# Determine if chunking is required (64MB is the limit for single upload method)
if os.path.getsize(file_path) >= FILESIZE_LIMIT:
logger.info('Publishing {0} to server with chunking method (datasource over 64MB)'.format(filename))
Expand All @@ -193,6 +197,12 @@ def publish(self, datasource_item, file_path, mode, connection_credentials=None,
connection_credentials,
connections)
server_response = self.post_request(url, xml_request, content_type)
new_datasource = DatasourceItem.from_response(server_response.content, self.parent_srv.namespace)[0]
logger.info('Published {0} (ID: {1})'.format(filename, new_datasource.id))
return new_datasource

if as_job:
new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
logger.info('Published {0} (JOB_ID: {1}'.format(filename, new_job.id))
return new_job
else:
new_datasource = DatasourceItem.from_response(server_response.content, self.parent_srv.namespace)[0]
logger.info('Published {0} (ID: {1})'.format(filename, new_datasource.id))
return new_datasource
17 changes: 13 additions & 4 deletions tableauserverclient/server/endpoint/workbooks_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,9 @@ def _get_wb_preview_image(self, workbook_item):

# Publishes workbook. Chunking method if file over 64MB
@api(version="2.0")
@parameter_added_in(as_job='3.0')
@parameter_added_in(connections='2.8')
def publish(self, workbook_item, file_path, mode, connection_credentials=None, connections=None):
def publish(self, workbook_item, file_path, mode, connection_credentials=None, connections=None, as_job=False):

if connection_credentials is not None:
import warnings
Expand Down Expand Up @@ -232,6 +233,9 @@ def publish(self, workbook_item, file_path, mode, connection_credentials=None, c
error = 'Workbooks cannot be appended.'
raise ValueError(error)

if as_job:
url += '&{0}=true'.format('asJob')

# Determine if chunking is required (64MB is the limit for single upload method)
if os.path.getsize(file_path) >= FILESIZE_LIMIT:
logger.info('Publishing {0} to server with chunking method (workbook over 64MB)'.format(filename))
Expand All @@ -253,6 +257,11 @@ def publish(self, workbook_item, file_path, mode, connection_credentials=None, c
connections=connections)
logger.debug('Request xml: {0} '.format(xml_request[:1000]))
server_response = self.post_request(url, xml_request, content_type)
new_workbook = WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)[0]
logger.info('Published {0} (ID: {1})'.format(filename, new_workbook.id))
return new_workbook
if as_job:
new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
logger.info('Published {0} (JOB_ID: {1}'.format(filename, new_job.id))
return new_job
else:
new_workbook = WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)[0]
logger.info('Published {0} (ID: {1})'.format(filename, new_workbook.id))
return new_workbook
4 changes: 4 additions & 0 deletions test/assets/datasource_publish_async.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?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-3.0.xsd">
<job id="9a373058-af5f-4f83-8662-98b3e0228a73" mode="Asynchronous" type="PublishDatasource" progress="0" createdAt="2018-06-30T00:54:54Z" finishCode="1"/>
</tsResponse>
4 changes: 4 additions & 0 deletions test/assets/workbook_publish_async.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?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-3.0.xsd">
<job id="7c3d599e-949f-44c3-94a1-f30ba85757e4" mode="Asynchronous" type="PublishWorkbook" progress="0" createdAt="2018-06-29T23:22:32Z" finishCode="1"/>
</tsResponse>
23 changes: 22 additions & 1 deletion test/test_datasource.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
GET_BY_ID_XML = 'datasource_get_by_id.xml'
POPULATE_CONNECTIONS_XML = 'datasource_populate_connections.xml'
PUBLISH_XML = 'datasource_publish.xml'
PUBLISH_XML_ASYNC = 'datasource_publish_async.xml'
10000 UPDATE_XML = 'datasource_update.xml'
UPDATE_CONNECTION_XML = 'datasource_connection_update.xml'

Expand Down Expand Up @@ -178,9 +179,11 @@ def test_publish(self):
with requests_mock.mock() as m:
m.post(self.baseurl, text=response_xml)
new_datasource = TSC.DatasourceItem('SampleDS', 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
publish_mode = self.server.PublishMode.CreateNew

new_datasource = self.server.datasources.publish(new_datasource,
asset('SampleDS.tds'),
mode=self.server.PublishMode.CreateNew)
mode=publish_mode)

self.assertEqual('e76a1461-3b1d-4588-bf1b-17551a879ad9', new_datasource.id)
self.assertEqual('SampleDS', new_datasource.name)
Expand All @@ -192,6 +195,24 @@ def test_publish(self):
self.assertEqual('default', new_datasource.project_name)
self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', new_datasource.owner_id)

def test_publish_async(self):
response_xml = read_xml_asset(PUBLISH_XML_ASYNC)
with requests_mock.mock() as m:
m.post(self.baseurl, text=response_xml)
new_datasource = TSC.DatasourceItem('SampleDS', 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
publish_mode = self.server.PublishMode.CreateNew

new_job = self.server.datasources.publish(new_datasource,
asset('SampleDS.tds'),
mode=publish_mode,
as_job=True)

self.assertEqual('9a373058-af5f-4f83-8662-98b3e0228a73', new_job.id)
self.assertEqual('PublishDatasource', new_job.type)
self.assertEqual('0', new_job.progress)
self.assertEqual('2018-06-30T00:54:54Z', format_datetime(new_job.created_at))
self.assertEqual('1', new_job.finish_code)

def test_delete(self):
with requests_mock.mock() as m:
m.delete(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb', status_code=204)
Expand Down
42 changes: 37 additions & 5 deletions test/test_workbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
POPULATE_VIEWS_XML = os.path.join(TEST_ASSET_DIR, 'workbook_populate_views.xml')
POPULATE_VIEWS_USAGE_XML = os.path.join(TEST_ASSET_DIR, 'workbook_populate_views_usage.xml')
PUBLISH_XML = os.path.join(TEST_ASSET_DIR, 'workbook_publish.xml')
PUBLISH_ASYNC_XML = os.path.join(TEST_ASSET_DIR, 'workbook_publish_async.xml')
UPDATE_XML = os.path.join(TEST_ASSET_DIR, 'workbook_update.xml')


Expand Down Expand Up @@ -290,10 +291,17 @@ def test_publish(self):
response_xml = f.read().decode('utf-8')
with requests_mock.mock() as m:
m.post(self.baseurl, text=response_xml)
new_workbook = TSC.WorkbookItem(name='Sample', show_tabs=False,

new_workbook = TSC.WorkbookItem(name='Sample',
show_tabs=False,
project_id='ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
new_workbook = self.server.workbooks.publish(new_workbook, os.path.join(TEST_ASSET_DIR, 'SampleWB.twbx'),
self.server.PublishMode.CreateNew)

sample_workbok = os.path.join(TEST_ASSET_DIR, 'SampleWB.twbx')
publish_mode = self.server.PublishMode.CreateNew

new_workbook = self.server.workbooks.publish(new_workbook,
sample_workbok,
publish_mode)

self.assertEqual('a8076ca1-e9d8-495e-bae6-c684dbb55836', new_workbook.id)
self.assertEqual('RESTAPISample', new_workbook.name)
Expand All @@ -309,10 +317,34 @@ def test_publish(self):
self.assertEqual('GDP per capita', new_workbook.views[0].name)
self.assertEqual('RESTAPISample_0/sheets/GDPpercapita', new_workbook.views[0].content_url)

def test_publish_async(self):
with open(PUBLISH_ASYNC_XML, 'rb') as f:
response_xml = f.read().decode('utf-8')
with requests_mock.mock() as m:
m.post(self.baseurl, text=response_xml)

new_workbook = TSC.WorkbookItem(name='Sample',
show_tabs=False,
project_id='ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')

sample_workbok = os.path.join(TEST_ASSET_DIR, 'SampleWB.twbx')
publish_mode = self.server.PublishMode.CreateNew

new_job = self.server.workbooks.publish(new_workbook,
sample_workbok,
publish_mode,
as_job=True)

self.assertEqual('7c3d599e-949f-44c3-94a1-f30ba85757e4', new_job.id)
self.assertEqual('PublishWorkbook', new_job.type)
self.assertEqual('0', new_job.progress)
self.assertEqual('2018-06-29T23:22:32Z', format_datetime(new_job.created_at))
self.assertEqual('1', new_job.finish_code)

def test_publish_invalid_file(self):
new_workbook = TSC.WorkbookItem('test', 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
self.assertRaises(IOError, self.server.workbooks.publish, new_workbook,
'.', self.server.PublishMode.CreateNew)
self.assertRaises(IOError, self.server.workbooks.publish, new_workbook, '.',
self.server.PublishMode.CreateNew)

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