8000 Multi-Credential Support in TSC by t8y8 · Pull Request #276 · tableau/server-client-python · GitHub
[go: up one dir, main page]

Skip to content

Multi-Credential Support in TSC #276

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 10 commits into from
Apr 20, 2018
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
19 changes: 18 additions & 1 deletion samples/publish_workbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import logging

import tableauserverclient as TSC
from tableauserverclient import ConnectionCredentials, ConnectionItem


def main():
Expand Down Expand Up @@ -50,10 +51,26 @@ def main():
all_projects, pagination_item = server.projects.get()
default_project = next((project for project in all_projects if project.is_default()), None)

connection1 = ConnectionItem()
connection1.server_address = "mssql.test.com"
connection1.connection_credentials = ConnectionCredentials("test", "password", True)

connection2 = ConnectionItem()
connection2.server_address = "postgres.test.com"
connection2.server_port = "5432"
connection2.connection_credentials = ConnectionCredentials("test", "password", True)

all_connections = list()
all_connections.append(connection1)
all_connections.append(connection2)

# 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)
new_workbook = server.workbooks.publish(new_workbook,
args.filepath,
overwrite_true,
connections=all_connections)
print("Workbook published. ID: {0}".format(new_workbook.id))
else:
error = "The default project could not be found."
Expand Down
12 changes: 12 additions & 0 deletions tableauserverclient/models/connection_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,15 @@ def oauth(self):
@property_is_boolean
def oauth(self, value):
self._oauth = value

@classmethod
def from_xml_element(cls, parsed_response, ns):
connection_creds_xml = parsed_response.find('.//t:connectionCredentials', namespaces=ns)

name = connection_creds_xml.get('name', None)
password = connection_creds_xml.get('password', None)
embed = connection_creds_xml.get('embed', None)
oAuth = connection_creds_xml.get('oAuth', None)

connection_creds = cls(name, password, embed, oAuth)
return connection_creds
31 changes: 31 additions & 0 deletions tableauserverclient/models/connection_item.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import xml.etree.ElementTree as ET
from .connection_credentials import ConnectionCredentials


class ConnectionItem(object):
Expand All @@ -12,6 +13,7 @@ def __init__(self):
self.server_address = None
self.server_port = None
self.username = None
self.connection_credentials = None

@property
def datasource_id(self):
Expand Down Expand Up @@ -51,3 +53,32 @@ def from_response(cls, resp, ns):
connection_item._datasource_name = datasource_elem.get('name', None)
all_connection_items.append(connection_item)
return all_connection_items

@classmethod
def from_xml_element(cls, parsed_response, ns):
'''
<connections>
<connection serverAddress="mysql.test.com">
<connectionCredentials embed="true" name="test" password="secret" />
</connection>
<connection serverAddress="pgsql.test.com">
<connectionCredentials embed="true" name="test" password="secret" />
</connection>
</connections>
'''
all_connection_items = list()
all_connection_xml = parsed_response.findall('.//t:connection', namespaces=ns)

for connection_xml in all_connection_xml:
connection_item = cls()

connection_item.server_address = connection_xml.get('serverAddress', None)
connection_item.server_port = connection_xml.get('serverPort', None)

connection_credentials = connection_xml.find('.//t:connectionCredentials', namespaces=ns)

if connection_credentials is not None:

connection_item.connection_credentials = ConnectionCredentials.from_xml_element(connection_credentials)

return all_connection_items
9 changes: 6 additions & 3 deletions tableauserverclient/server/endpoint/datasources_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ def refresh(self, datasource_item):

# Publish datasource
@api(version="2.0")
def publish(self, datasource_item, file_path, mode, connection_credentials=None):
@parameter_added_in(connections="99.99")
def publish(self, datasource_item, file_path, mode, connection_credentials=None, connections=None):
if not os.path.isfile(file_path):
error = "File path does not lead to an existing file."
raise IOError(error)
Expand Down Expand Up @@ -180,15 +181,17 @@ def publish(self, datasource_item, file_path, mode, connection_credentials=None)
upload_session_id = Fileuploads.upload_chunks(self.parent_srv, file_path)
url = "{0}&uploadSessionId={1}".format(url, upload_session_id)
xml_request, content_type = RequestFact 8000 ory.Datasource.publish_req_chunked(datasource_item,
connection_credentials)
connection_credentials,
connections)
else:
logger.info('Publishing {0} to server'.format(filename))
with open(file_path, 'rb') as f:
file_contents = f.read()
xml_request, content_type = RequestFactory.Datasource.publish_req(datasource_item,
filename,
file_contents,
connection_credentials)
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))
Expand Down
2 changes: 1 addition & 1 deletion tableauserverclient/server/endpoint/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def _safe_to_log(server_response):
'''Checks if the server_response content is not xml (eg binary image or zip)
and and replaces it with a constant
'''
ALLOWED_CONTENT_TYPES = ('application/xml',)
ALLOWED_CONTENT_TYPES = ('application/xml', 'application/xml;charset=utf-8')
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I should allow this type through as well, missed it in first PR

if server_response.headers.get('Content-Type', None) not in ALLOWED_CONTENT_TYPES:
return '[Truncated File Contents]'
else:
Expand Down
18 changes: 15 additions & 3 deletions tableauserverclient/server/endpoint/workbooks_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,14 @@ def _get_wb_preview_image(self, workbook_item):

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

if connection_credentials is not None:
import warnings
warnings.warn("connection_credentials is being deprecated. Use connections instead",
DeprecationWarning)

if not os.path.isfile(file_path):
error = "File path does not lead to an existing file."
raise IOError(error)
Expand Down Expand Up @@ -230,16 +237,21 @@ def publish(self, workbook_item, file_path, mode, connection_credentials=None):
logger.info('Publishing {0} to server with chunking method (workbook over 64MB)'.format(filename))
upload_session_id = Fileuploads.upload_chunks(self.parent_srv, file_path)
url = "{0}&uploadSessionId={1}".format(url, upload_session_id)
conn_creds = connection_credentials
Copy link
Contributor

Choose a reason for hiding this comment

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

Is conn_creds variable necessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Only because of line-length limits 🤷‍♂️

xml_request, content_type = RequestFactory.Workbook.publish_req_chunked(workbook_item,
connection_credentials)
connection_credentials=conn_creds,
connections=connections)
else:
logger.info('Publishing {0} to server'.format(filename))
with open(file_path, 'rb') as f:
file_contents = f.read()
conn_creds = connection_credentials
xml_request, content_type = RequestFactory.Workbook.publish_req(workbook_item,
filename,
file_contents,
connection_credentials)
connection_credentials=conn_creds,
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))
Expand Down
79 changes: 54 additions & 25 deletions tableauserverclient/server/request_factory.py
< 10000 td class="blob-num blob-num-addition empty-cell">
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ def wrapper(self, *args, **kwargs):
return wrapper


def _add_connections_element(connections_element, connection):
connection_element = ET.SubElement(connections_element, 'connection')
connection_element.attrib['serverAddress'] = connection.server_address
if connection.server_port:
connection_element.attrib['serverPort'] = connection.server_port
if connection.connection_credentials:
connection_credentials = connection.connection_credentials
_add_credentials_element(connection_element, connection_credentials)


def _add_credentials_element(parent_element, connection_credentials):
credentials_element = ET.SubElement(parent_element, 'connectionCredentials')
credentials_element.attrib['name'] = connection_credentials.name
credentials_element.attrib['password'] = connection_credentials.password
credentials_element.attrib['embed'] = 'true' if connection_credentials.embed else 'false'
if connection_credentials.oauth:
credentials_element.attrib['oAuth'] = 'true'


class AuthRequest(object):
def signin_req(self, auth_item):
xml_request = ET.Element('tsRequest')
Expand All @@ -40,20 +59,23 @@ def signin_req(self, auth_item):


class DatasourceRequest(object):
def _generate_xml(self, datasource_item, connection_credentials=None):
def _generate_xml(self, datasource_item, connection_credentials=None, connections=None):
xml_request = ET.Element('tsRequest')
datasource_element = ET.SubElement(xml_request, 'datasource')
datasource_element.attrib['name'] = datasource_item.name
project_element = ET.SubElement(datasource_element, 'project')
project_element.attrib['id'] = datasource_item.project_id
if connection_credentials:
credentials_element = ET.SubElement(datasource_element, 'connectionCredentials')
credentials_element.attrib['name'] = connection_credentials.name
credentials_element.attrib['password'] = connection_credentials.password
credentials_element.attrib['embed'] = 'true' if connection_credentials.embed else 'false'

if connection_credentials.oauth:
credentials_element.attrib['oAuth'] = 'true'

if connection_credentials is not None and connections is not None:
raise RuntimeError('You cannot set both `connections` and `connection_credentials`')

if connection_credentials is not None:
_add_credentials_element(datasource_element, connection_credentials)

if connections is not None:
connections_element = ET.SubElement(datasource_element, 'connections')
for connection in connections:
_add_connections_element(connections_element, connection)
return ET.tostring(xml_request)

def update_req(self, datasource_item):
Expand All @@ -73,15 +95,15 @@ def update_req(self, datasource_item):

return ET.tostring(xml_request)

def publish_req(self, datasource_item, filename, file_contents, connection_credentials=None):
xml_request = self._generate_xml(datasource_item, connection_credentials)
def publish_req(self, datasource_item, filename, file_contents, connection_credentials=None, connections=None):
xml_request = self._generate_xml(datasource_item, connection_credentials, connections)

parts = {'request_payload': ('', xml_request, 'text/xml'),
'tableau_datasource': (filename, file_contents, 'application/octet-stream')}
return _add_multipart(parts)

def publish_req_chunked(self, datasource_item, connection_credentials=None):
xml_request = self._generate_xml(datasource_item, connection_credentials)
xml_request = self._generate_xml(datasource_item, connection_credentials, connections)

parts = {'request_payload': ('', xml_request, 'text/xml')}
return _add_multipart(parts)
Expand Down Expand Up @@ -313,22 +335,25 @@ def add_req(self, user_item):


class WorkbookRequest(object):
def _generate_xml(self, workbook_item, connection_credentials=None):
def _generate_xml(self, workbook_item, connection_credentials=None, connections=None):
xml_request = ET.Element('tsRequest')
workbook_element = ET.SubElement(xml_request, 'workbook')
workbook_element.attrib['name'] = workbook_item.name
if workbook_item.show_tabs:
workbook_element.attrib['showTabs'] = str(workbook_item.show_tabs).lower()
project_element = ET.SubElement(workbook_element, 'project')
project_element.attrib['id'] = workbook_item.project_id
if connection_credentials:
credentials_element = ET.SubElement(workbook_element, 'connectionCredentials')
credentials_element.attrib['name'] = connection_credentials.name
credentials_element.attrib['password'] = connection_credentials.password
credentials_element.attrib['embed'] = 'true' if connection_credentials.embed else 'false'

if connection_credentials.oauth:
credentials_element.attrib['oAuth'] = 'true'

if connection_credentials is not None and connections is not None:
raise RuntimeError('You cannot set both `connections` and `connection_credentials`')

if connection_credentials is not None:
_add_credentials_element(workbook_element, connection_credentials)

if connections is not None:
connections_element = ET.SubElement(workbook_element, 'connections')
for connection in connections:
_add_connections_element(connections_element, connection)
return ET.tostring(xml_request)

def update_req(self, workbook_item):
Expand All @@ -344,15 +369,19 @@ def update_req(self, workbook_item):
owner_element.attrib['id'] = workbook_item.owner_id
return ET.tostring(xml_request)

def publish_req(self, workbook_item, filename, file_contents, connection_credentials=None):
xml_request = self._generate_xml(workbook_item, connection_credentials)
def publish_req(self, workbook_item, filename, file_contents, connection_credentials=None, connections=None):
xml_request = self._generate_xml(workbook_item,
connection_credentials=connection_credentials,
connections=connections)

parts = {'request_payload': ('', xml_request, 'text/xml'),
'tableau_workbook': (filename, file_contents, 'application/octet-stream')}
return _add_multipart(parts)

def publish_req_chunked(self, workbook_item, connection_credentials=None):
xml_request = self._generate_xml(workbook_item, connection_credentials)
def publish_req_chunked(self, workbook_item, connections=None):
xml_request = self._generate_xml(workbook_item,
connection_credentials=connection_credentials,
connections=connections)

parts = {'request_payload': ('', xml_request, 'text/xml')}
return _add_multipart(parts)
Expand Down
47 changes: 47 additions & 0 deletions test/test_datasource.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import unittest
import os
import requests_mock
import xml.etree.ElementTree as ET
import tableauserverclient as TSC
from tableauserverclient.datetime_helpers import format_datetime
from tableauserverclient.server.request_factory import RequestFactory
from ._utils import read_xml_asset, read_xml_assets, asset

ADD_TAGS_XML = 'datasource_add_tags.xml'
Expand Down Expand Up @@ -241,3 +243,48 @@ def test_publish_invalid_file_type(self):
new_datasource = TSC.DatasourceItem('test', 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
self.assertRaises(ValueError, self.server.datasources.publish, new_datasource,
asset('SampleWB.twbx'), self.server.PublishMode.Append)

def test_publish_multi_connection(self):
new_datasource = TSC.DatasourceItem(name='Sample', project_id='ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
connection1 = TSC.ConnectionItem()
connection1.server_address = 'mysql.test.com'
connection1.connection_credentials = TSC.ConnectionCredentials('test', 'secret', True)
connection2 = TSC.ConnectionItem()
connection2.server_address = 'pgsql.test.com'
connection2.connection_credentials = TSC.ConnectionCredentials('test', 'secret', True)

response = RequestFactory.Datasource._generate_xml(new_datasource, connections=[connection1, connection2])
# Can't use ConnectionItem parser due to xml namespace problems
connection_results = ET.fromstring(response).findall('.//connection')

self.assertEqual(connection_results[0].get('serverAddress', None), 'mysql.test.com')
self.assertEqual(connection_results[0].find('connectionCredentials').get('name', None), 'test')
self.assertEqual(connection_results[1].get('serverAddress', None), 'pgsql.test.com')
self.assertEqual(connection_results[1].find('connectionCredentials').get('password', None), 'secret')

def test_publish_single_connection(self):
new_datasource = TSC.DatasourceItem(name='Sample', project_id='ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
connection_creds = TSC.ConnectionCredentials('test', 'secret', True)

response = RequestFactory.Datasource._generate_xml(new_datasource, connection_credentials=connection_creds)
# Can't use ConnectionItem parser due to xml namespace problems
credentials = ET.fromstring(response).findall('.//connectionCredentials')

self.assertEqual(len(credentials), 1)
self.assertEqual(credentials[0].get('name', None), 'test')
self.assertEqual(credentials[0].get('password', None), 'secret')
self.assertEqual(credentials[0].get('embed', None), 'true')

def test_credentials_and_multi_connect_raises_exception(self):
new_datasource = TSC.DatasourceItem(name='Sample', project_id='ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')

connection_creds = TSC.ConnectionCredentials('test', 'secret', True)

connection1 = TSC.ConnectionItem()
connection1.server_address = 'mysql.test.com'
connection1.connection_credentials = TSC.ConnectionCredentials('test', 'secret', True)

with self.assertRaises(RuntimeError):
response = RequestFactory.Datasource._generate_xml(new_datasource,
connection_credentials=connection_creds,
connections=[connection1])
Loading
0