Skip to content

Commit a1179eb

Browse files
author
Russell Hay
authored
tableau#190 add update datasource connection (tableau#253)
* checkpoint in getting update connection to work. Still need to add error handling * Added tests and correcting the return type * Address Tyler's feedback * missed one of the changes from Tyler's feedback
1 parent 8c24344 commit a1179eb

File tree

9 files changed

+164
-42
lines changed

9 files changed

+164
-42
lines changed

samples/update_connection.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
####
2+
# This script demonstrates how to update a connections credentials on a server to embed the credentials
3+
#
4+
# To run the script, you must have installed Python 2.7.X or 3.3 and later.
5+
####
6+
7+
import argparse
8+
import getpass
9+
import logging
10+
11+
import tableauserverclient as TSC
12+
13+
14+
def main():
15+
parser = argparse.ArgumentParser(description='Update a connection on a datasource or workbook to embed credentials')
16+
parser.add_argument('--server', '-s', required=True, help='server address')
17+
parser.add_argument('--username', '-u', required=True, help='username to sign into server')
18+
parser.add_argument('--site', '-S', default=None)
19+
parser.add_argument('-p', default=None)
20+
21+
parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error',
22+
help='desired logging level (set to error by default)')
23+
24+
parser.add_argument('resource_type', choices=['workbook', 'datasource'])
25+
parser.add_argument('resource_id')
26+
parser.add_argument('connection_id')
27+
parser.add_argument('datasource_username')
28+
parser.add_argument('datasource_password')
29+
30+
args = parser.parse_args()
31+
32+
if args.p is None:
33+
password = getpass.getpass("Password: ")
34+
else:
35+
password = args.p
36+
37+
# Set logging level based on user input, or error by default
38+
logging_level = getattr(logging, args.logging_level.upper())
39+
logging.basicConfig(level=logging_level)
40+
41+
# SIGN IN
42+
tableau_auth = TSC.TableauAuth(args.username, password, args.site)
43+
server = TSC.Server(args.server, use_server_version=True)
44+
with server.auth.sign_in(tableau_auth):
45+
endpoint = {
46+
'workbook': server.workbooks,
47+
'datasource': server.datasources
48+
}.get(args.resource_type)
49+
50+
update_function = endpoint.update_connection
51+
resource = endpoint.get_by_id(args.resource_id)
52+
endpoint.populate_connections(resource)
53+
connections = list(filter(lambda x: x.id == args.connection_id, resource.connections))
54+
assert(len(connections) == 1)
55+
connection = connections[0]
56+
connection.username = args.datasource_username
57+
connection.password = args.datasource_password
58+
connection.embed_password = True
59+
print(update_function(resource, connection).content)
60+
61+
62+
if __name__ == '__main__':
63+
main()

tableauserverclient/models/connection_item.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ def id(self):
2929
def connection_type(self):
3030
return self._connection_type
3131

32+
def __repr__(self):
33+
return "<ConnectionItem#{_id} embed={embed_password} type={_connection_type} username={username}>"\
34+
.format(**self.__dict__)
35+
3236
@classmethod
3337
def from_response(cls, resp, ns):
3438
all_connection_items = list()

tableauserverclient/server/endpoint/datasources_endpoint.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,19 @@ def update(self, datasource_item):
129129
updated_datasource = copy.copy(datasource_item)
130130
return updated_datasource._parse_common_elements(server_response.content, self.parent_srv.namespace)
131131

132+
# Update datasource connections
133+
@api(version="2.3")
134+
def update_connection(self, datasource_item, connection_item):
135+
url = "{0}/{1}/connections/{2}".format(self.baseurl, datasource_item.id, connection_item.id)
136+
137+
update_req = RequestFactory.Connection.update_req(connection_item)
138+
server_response = self.put_request(url, update_req)
139+
connection = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0]
140+
141+
logger.info('Updated datasource item (ID: {0} & connection item {1}'.format(datasource_item.id,
142+
connection_item.id))
143+
return connection
144+
132145
def refresh(self, datasource_item):
133146
url = "{0}/{1}/refresh".format(self.baseurl, datasource_item.id)
134147
empty_req = RequestFactory.Empty.empty_req()

tableauserverclient/server/endpoint/workbooks_endpoint.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,23 @@ def update(self, workbook_item):
8686
updated_workbook = copy.copy(workbook_item)
8787
return updated_workbook._parse_common_tags(server_response.content, self.parent_srv.namespace)
8888

89+
@api(version="2.3")
90+
def update_conn(self, *args, **kwargs):
91+
import warnings
92+
warnings.warn('update_conn is deprecated, please use update_connection instead')
93+
return self.update_connection(*args, **kwargs)
94+
8995
# Update workbook_connection
90-
def update_conn(self, workbook_item, connection_item):
96+
@api(version="2.3")
97+
def update_connection(self, workbook_item, connection_item):
9198
url = "{0}/{1}/connections/{2}".format(self.baseurl, workbook_item.id, connection_item.id)
92-
update_req = RequestFactory.WorkbookConnection.update_req(connection_item)
99+
update_req = RequestFactory.Connection.update_req(connection_item)
93100
server_response = self.put_request(url, update_req)
94-
logger.info('Updated workbook item (ID: {0} & connection item {1}'.format(workbook_item.id, connection_item.id))
101+
connection = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0]
102+
103+
logger.info('Updated workbook item (ID: {0} & connection item {1}'.format(workbook_item.id,
104+
connection_item.id))
105+
return connection
95106

96107
# Download workbook contents with option of passing in filepath
97108
@api(version="2.0")

tableauserverclient/server/request_factory.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def _add_multipart(parts):
2020
def _tsrequest_wrapped(func):
2121
def wrapper(self, *args, **kwargs):
2222
xml_request = ET.Element('tsRequest')
23-
func(xml_request, *args, **kwargs)
23+
func(self, xml_request, *args, **kwargs)
2424
return ET.tostring(xml_request)
2525
return wrapper
2626

@@ -358,9 +358,9 @@ def publish_req_chunked(self, workbook_item, connection_credentials=None):
358358
return _add_multipart(parts)
359359

360360

361-
class WorkbookConnection(object):
362-
def update_req(self, connection_item):
363-
xml_request = ET.Element('tsRequest')
361+
class Connection(object):
362+
@_tsrequest_wrapped
363+
def update_req(self, xml_request, connection_item):
364364
connection_element = ET.SubElement(xml_request, 'connection')
365365
if connection_item.server_address:
366366
connection_element.attrib['serverAddress'] = connection_item.server_address.lower()
@@ -371,13 +371,12 @@ def update_req(self, connection_item):
371371
if connection_item.password:
372372
connection_element.attrib['password'] = connection_item.password
373373
if connection_item.embed_password:
374-
connection_element.attrib['embedPassword'] = connection_item.embed_password
375-
return ET.tostring(xml_request)
374+
connection_element.attrib['embedPassword'] = str(connection_item.embed_password)
376375

377376

378377
class TaskRequest(object):
379378
@_tsrequest_wrapped
380-
def run_req(xml_request, task_item):
379+
def run_req(self, xml_request, task_item):
381380
# Send an empty tsRequest
382381
pass
383382

@@ -408,6 +407,7 @@ def empty_req(xml_request):
408407

409408
class RequestFactory(object):
410409
Auth = AuthRequest()
410+
Connection = Connection()
411411
Datasource = DatasourceRequest()
412412
Empty = EmptyRequest()
413413
Fileupload = FileuploadRequest()
@@ -420,5 +420,4 @@ class RequestFactory(object):
420420
Task = TaskRequest()
421421
User = UserRequest()
422422
Workbook = WorkbookRequest()
423-
WorkbookConnection = WorkbookConnection()
424423
Subscription = SubscriptionRequest()

test/_utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import os.path
2+
3+
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets')
4+
5+
6+
def asset(filename):
7+
return os.path.join(TEST_ASSET_DIR, filename)
8+
9+
10+
def read_xml_asset(filename):
11+
with open(asset(filename), 'rb') as f:
12+
return f.read().decode('utf-8')
13+
14+
15+
def read_xml_assets(*args):
16+
return map(read_xml_asset, args)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.8.xsd">
4+
<connection id="be786ae0-d2bf-4a4b-9b34-e2de8d2d4488"
5+
type="textscan" userName="foo"/></tsResponse>

test/test_datasource.py

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
import requests_mock
44
import tableauserverclient as TSC
55
from tableauserverclient.datetime_helpers import format_datetime
6+
from ._utils import read_xml_asset, read_xml_assets, asset
67

7-
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets')
8-
9-
ADD_TAGS_XML = os.path.join(TEST_ASSET_DIR, 'datasource_add_tags.xml')
10-
GET_XML = os.path.join(TEST_ASSET_DIR, 'datasource_get.xml')
11-
GET_EMPTY_XML = os.path.join(TEST_ASSET_DIR, 'datasource_get_empty.xml')
12-
GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, 'datasource_get_by_id.xml')
13-
POPULATE_CONNECTIONS_XML = os.path.join(TEST_ASSET_DIR, 'datasource_populate_connections.xml')
14-
PUBLISH_XML = os.path.join(TEST_ASSET_DIR, 'datasource_publish.xml')
15-
UPDATE_XML = os.path.join(TEST_ASSET_DIR, 'datasource_update.xml')
8+
ADD_TAGS_XML = 'datasource_add_tags.xml'
9+
GET_XML = 'datasource_get.xml'
10+
GET_EMPTY_XML = 'datasource_get_empty.xml'
11+
GET_BY_ID_XML = 'datasource_get_by_id.xml'
12+
POPULATE_CONNECTIONS_XML = 'datasource_populate_connections.xml'
13+
PUBLISH_XML = 'datasource_publish.xml'
14+
UPDATE_XML = 'datasource_update.xml'
15+
UPDATE_CONNECTION_XML = 'datasource_connection_update.xml'
1616

1717

1818
class DatasourceTests(unittest.TestCase):
@@ -26,8 +26,7 @@ def setUp(self):
2626
self.baseurl = self.server.datasources.baseurl
2727

2828
def test_get(self):
29-
with open(GET_XML, 'rb') as f:
30-
response_xml = f.read().decode('utf-8')
29+
response_xml = read_xml_asset(GET_XML)
3130
with requests_mock.mock() as m:
3231
m.get(self.baseurl, text=response_xml)
3332
all_datasources, pagination_item = self.server.datasources.get()
@@ -59,8 +58,7 @@ def test_get_before_signin(self):
5958
self.assertRaises(TSC.NotSignedInError, self.server.datasources.get)
6059

6160
def test_get_empty(self):
62-
with open(GET_EMPTY_XML, 'rb') as f:
63-
response_xml = f.read().decode('utf-8')
61+
response_xml = read_xml_asset(GET_EMPTY_XML)
6462
with requests_mock.mock() as m:
6563
m.get(self.baseurl, text=response_xml)
6664
all_datasources, pagination_item = self.server.datasources.get()
@@ -69,8 +67,7 @@ def test_get_empty(self):
6967
self.assertEqual([], all_datasources)
7068

7169
def test_get_by_id(self):
72-
with open(GET_BY_ID_XML, 'rb') as f:
73-
response_xml = f.read().decode('utf-8')
70+
response_xml = read_xml_asset(GET_BY_ID_XML)
7471
with requests_mock.mock() as m:
7572
m.get(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb', text=response_xml)
7673
single_datasource = self.server.datasources.get_by_id('9dbd2263-16b5-46e1-9c43-a76bb8ab65fb')
@@ -87,8 +84,7 @@ def test_get_by_id(self):
8784
self.assertEqual(set(['world', 'indicators', 'sample']), single_datasource.tags)
8885

8986
def test_update(self):
90-
with open(UPDATE_XML, 'rb') as f:
91-
response_xml = f.read().decode('utf-8')
87+
response_xml = read_xml_asset(UPDATE_XML)
9288
with requests_mock.mock() as m:
9389
m.put(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb', text=response_xml)
9490
single_datasource = TSC.DatasourceItem('test', '1d0304cd-3796-429f-b815-7258370b9b74')
@@ -105,7 +101,7 @@ def test_update(self):
105101
self.assertEqual("Warning, here be dragons.", single_datasource.certification_note)
106102

107103
def test_update_copy_fields(self):
108-
with open(UPDATE_XML, 'rb') as f:
104+
with open(asset(UPDATE_XML), 'rb') as f:
109105
response_xml = f.read().decode('utf-8')
110106
with requests_mock.mock() as m:
111107
m.put(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb', text=response_xml)
@@ -118,10 +114,7 @@ def test_update_copy_fields(self):
118114
self.assertEqual(single_datasource._project_name, updated_datasource._project_name)
119115

120116
def test_update_tags(self):
121-
with open(ADD_TAGS_XML, 'rb') as f:
122-
add_tags_xml = f.read().decode('utf-8')
123-
with open(UPDATE_XML, 'rb') as f:
124-
update_xml = f.read().decode('utf-8')
117+
add_tags_xml, update_xml = read_xml_assets(ADD_TAGS_XML, UPDATE_XML)
125118
with requests_mock.mock() as m:
126119
m.put(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/tags', text=add_tags_xml)
127120
m.delete(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/tags/b', status_code=204)
@@ -137,8 +130,7 @@ def test_update_tags(self):
137130
self.assertEqual(single_datasource._initial_tags, updated_datasource._initial_tags)
138131

139132
def test_populate_connections(self):
140-
with open(POPULATE_CONNECTIONS_XML, 'rb') as f:
141-
response_xml = f.read().decode('utf-8')
133+
response_xml = read_xml_asset(POPULATE_CONNECTIONS_XML)
142134
with requests_mock.mock() as m:
143135
m.get(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections', text=response_xml)
144136
single_datasource = TSC.DatasourceItem('test', '1d0304cd-3796-429f-b815-7258370b9b74')
@@ -155,14 +147,33 @@ def test_populate_connections(self):
155147
self.assertEqual(ds2.id, '970e24bc-e200-4841-a3e9-66e7d122d77e')
156148
self.assertEqual(ds3.id, '7d85b889-283b-42df-b23e-3c811e402f1f')
157149

150+
def test_update_connection(self):
151+
populate_xml, response_xml = read_xml_assets(POPULATE_CONNECTIONS_XML, UPDATE_CONNECTION_XML)
152+
153+
with requests_mock.mock() as m:
154+
m.get(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections', text=populate_xml)
155+
m.put(self.baseurl +
156+
'/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections/be786ae0-d2bf-4a4b-9b34-e2de8d2d4488',
157+
text=response_xml)
158+
single_datasource = TSC.DatasourceItem('test', '1d0304cd-3796-429f-b815-7258370b9b74')
159+
single_datasource.owner_id = 'dd2239f6-ddf1-4107-981a-4cf94e415794'
160+
single_datasource._id = '9dbd2263-16b5-46e1-9c43-a76bb8ab65fb'
161+
self.server.datasources.populate_connections(single_datasource)
162+
163+
connection = single_datasource.connections[0]
164+
connection.username = 'foo'
165+
new_connection = self.server.datasources.update_connection(single_datasource, connection)
166+
self.assertEqual(connection.id, new_connection.id)
167+
self.assertEqual(connection.connection_type, new_connection.connection_type)
168+
self.assertEqual('foo', new_connection.username)
169+
158170
def test_publish(self):
159-
with open(PUBLISH_XML, 'rb') as f:
160-
response_xml = f.read().decode('utf-8')
171+
response_xml = read_xml_asset(PUBLISH_XML)
161172
with requests_mock.mock() as m:
162173
m.post(self.baseurl, text=response_xml)
163174
new_datasource = TSC.DatasourceItem('SampleDS', 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
164175
new_datasource = self.server.datasources.publish(new_datasource,
165-
os.path.join(TEST_ASSET_DIR, 'SampleDS.tds'),
176+
asset('SampleDS.tds'),
166177
mode=self.server.PublishMode.CreateNew)
167178

168179
self.assertEqual('e76a1461-3b1d-4588-bf1b-17551a879ad9', new_datasource.id)
@@ -224,9 +235,9 @@ def test_publish_missing_path(self):
224235
def test_publish_missing_mode(self):
225236
new_datasource = TSC.DatasourceItem('test', 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
226237
self.assertRaises(ValueError, self.server.datasources.publish, new_datasource,
227-
os.path.join(TEST_ASSET_DIR, 'SampleDS.tds'), None)
238+
asset('SampleDS.tds'), None)
228239

229240
def test_publish_invalid_file_type(self):
230241
new_datasource = TSC.DatasourceItem('test', 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
231242
self.assertRaises(ValueError, self.server.datasources.publish, new_datasource,
232-
os.path.join(TEST_ASSET_DIR, 'SampleWB.twbx'), self.server.PublishMode.Append)
243+
asset('SampleWB.twbx'), self.server.PublishMode.Append)

test/test_schedule.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ def test_add_workbook(self):
198198
m.put(baseurl + '/foo/workbooks', text="OK")
199199
workbook = self.server.workbooks.get_by_id("bar")
200200
result = self.server.schedules.add_to_schedule('foo', workbook=workbook)
201-
self.assertEquals(0, len(result), "Added properly")
201+
self.assertEqual(0, len(result), "Added properly")
202202

203203
def test_add_datasource(self):
204204
self.server.version = "2.8"
@@ -212,4 +212,4 @@ def test_add_datasource(self):
212212
m.put(baseurl + '/foo/datasources', text="OK")
213213
datasource = self.server.datasources.get_by_id("bar")
214214
result = self.server.schedules.add_to_schedule('foo', datasource=datasource)
215-
self.assertEquals(0, len(result), "Added properly")
215+
self.assertEqual(0, len(result), "Added properly")

0 commit comments

Comments
 (0)