8000 Initial support for Tableau Metadata API · SnarkyPapi/server-client-python@07030af · GitHub
[go: up one dir, main page]

Skip to content

Commit 07030af

Browse files
committed
Initial support for Tableau Metadata API
1 parent 8fd8d78 commit 07030af

File tree

7 files changed

+165
-2
lines changed

7 files changed

+165
-2
lines changed

tableauserverclient/server/endpoint/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .exceptions import ServerResponseError, MissingRequiredFieldError, ServerInfoEndpointNotFoundError
55
from .groups_endpoint import Groups
66
from .jobs_endpoint import Jobs
7+
from .metadata_endpoint import Metadata
78
from .projects_endpoint import Projects
89
from .schedules_endpoint import Schedules
910
from .server_info_endpoint import ServerInfo

tableauserverclient/server/endpoint/exceptions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,12 @@ class EndpointUnavailableError(Exception):
4444

4545
class ItemTypeNotAllowed(Exception):
4646
pass
47+
48+
49+
class GraphQLError(Exception):
50+
def __init__(self, error_payload):
51+
self.error = error_payload
52+
53+
def __str__(self):
54+
from pprint import pformat
55+
return pformat(self.error)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from .endpoint import Endpoint, api
2+
from .exceptions import GraphQLError
3+
import logging
4+
import json
5+
6+
logger = logging.getLogger('tableau.endpoint.metadata')
7+
8+
9+
class Metadata(Endpoint):
10+
@property
11+
def baseurl(self):
12+
return "{0}/api/exp/metadata/graphql".format(self.parent_srv._server_address)
13+
14+
@api("3.2")
15+
def query(self, query, abort_on_error=False):
16+
logger.info('Querying Metadata API')
17+
url = self.baseurl
18+
19+
try:
20+
graphql_query = json.dumps({'query': query})
21+
except Exception:
22+
# Place holder for now
23+
raise Exception('Must provide a string')
24+
25+
# Setting content type because post_reuqest defaults to text/xml
26+
server_response = self.post_request(url, graphql_query, content_type='text/json')
27+
results = json.loads(server_response.content)
28+
29+
if abort_on_error and results.get('errors', None):
30+
raise GraphQLError(results['errors'])
31+
32+
return results

tableauserverclient/server/server.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from .exceptions import NotSignedInError
44
from ..namespace import Namespace
55
from .endpoint import Sites, Views, Users, Groups, Workbooks, Datasources, Projects, Auth, \
6-
Schedules, ServerInfo, Tasks, ServerInfoEndpointNotFoundError, Subscriptions, Jobs
7-
from .endpoint.exceptions import EndpointUnavailableError
6+
Schedules, ServerInfo, Tasks, ServerInfoEndpointNotFoundError, Subscriptions, Jobs, Metadata
7+
from .endpoint.exceptions import EndpointUnavailableError, ServerInfoEndpointNotFoundError
88

99
import requests
1010

@@ -50,6 +50,7 @@ def __init__(self, server_address, use_server_version=False):
5050
self.server_info = ServerInfo(self)
5151
self.tasks = Tasks(self)
5252
self.subscriptions = Subscriptions(self)
53+
self.metadata = Metadata(self)
5354
self._namespace = Namespace()
5455

5556
if use_server_version:

test/assets/metadata_query_error.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"data": {
3+
"publishedDatasources": [
4+
{
5+
"id": "01cf92b2-2d17-b656-fc48-5c25ef6d5352",
6+
"name": "Batters (TestV1)"
7+
},
8+
{
9+
"id": "020ae1cd-c356-f1ad-a846-b0094850d22a",
10+
"name": "SharePoint_List_sharepoint2010.test.tsi.lan"
11+
},
12+
{
13+
"id": "061493a0-c3b2-6f39-d08c-bc3f842b44af",
14+
"name": "Batters_mongodb"
15+
},
16+
{
17+
"id": "089fe515-ad2f-89bc-94bd-69f55f69a9c2",
18+
"name": "Sample - Superstore"
19+
}
20+
]
21+
},
22+
"errors": [
23+
{
24+
"message": "Reached time limit of PT5S for query execution.",
25+
"path": null,
26+
"extensions": null
27+
}
28+
]
29+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"data": {
3+
"publishedDatasources": [
4+
{
5+
"id": "01cf92b2-2d17-b656-fc48-5c25ef6d5352",
6+
"name": "Batters (TestV1)"
7+
},
8+
{
9+
"id": "020ae1cd-c356-f1ad-a846-b0094850d22a",
10+
"name": "SharePoint_List_sharepoint2010.test.tsi.lan"
11+
},
12+
{
13+
"id": "061493a0-c3b2-6f39-d08c-bc3f842b44af",
14+
"name": "Batters_mongodb"
15+
},
16+
{
17+
"id": "089fe515-ad2f-89bc-94bd-69f55f69a9c2",
18+
"name": "Sample - Superstore"
19+
}
20+
]
21+
}
22+
}

test/test_metadata.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import unittest
2+
import os.path
3+
import json
4+
import requests_mock
5+
import tableauserverclient as TSC
6+
7+
from tableauserverclient.server.endpoint.exceptions import GraphQLError
8+
9+
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets')
10+
11+
METADATA_QUERY_SUCCESS = os.path.join(TEST_ASSET_DIR, 'metadata_query_success.json')
12+
METADATA_QUERY_ERROR = os.path.join(TEST_ASSET_DIR, 'metadata_query_error.json')
13+
14+
EXPECTED_DICT = {'publishedDatasources':
15+
[{'id': '01cf92b2-2d17-b656-fc48-5c25ef6d5352', 'name': 'Batters (TestV1)'},
16+
{'id': '020ae1cd-c356-f1ad-a846-b0094850d22a', 'name': 'SharePoint_List_sharepoint2010.test.tsi.lan'},
17+
{'id': '061493a0-c3b2-6f39-d08c-bc3f842b44af', 'name': 'Batters_mongodb'},
18+
{'id': '089fe515-ad2f-89bc-94bd-69f55f69a9c2', 'name': 'Sample - Superstore'}]}
19+
20+
EXPECTED_DICT_ERROR = [
21+
{
22+
"message": "Reached time limit of PT5S for query execution.",
23+
"path": None,
24+
"extensions": None
25+
}
26+
]
27+
28+
29+
class MetadataTests(unittest.TestCase):
30+
def setUp(self):
31+
self.server = TSC.Server('http://test')
32+
self.baseurl = self.server.metadata.baseurl
33+
self.server.version = "3.2"
34+
35+
self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67'
36+
self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM'
37+
38+
def test_metadata_query(self):
39+
with open(METADATA_QUERY_SUCCESS, 'rb') as f:
40+
response_json = json.load(f)
41+
with requests_mock.mock() as m:
42+
m.post(self.baseurl, json=response_json)
43+
actual = self.server.metadata.query('fake query')
44+
45+
datasources = actual['data']
46+
47+
self.assertDictEqual(EXPECTED_DICT, datasources)
48+
49+
def test_metadata_query_ignore_error(self):
50+
with open(METADATA_QUERY_ERROR, 'rb') as f:
51+
response_json = json.load(f)
52+
with requests_mock.mock() as m:
53+
m.post(self.baseurl, json=response_json)
54+
actual = self.server.metadata.query('fake query')
55+
datasources = actual['data']
56+
57+
self.assertNotEqual(actual.get('errors', None), None)
58+
self.assertListEqual(EXPECTED_DICT_ERROR, actual['errors'])
59+
self.assertDictEqual(EXPECTED_DICT, datasources)
60+
61+
def test_metadata_query_abort_on_error(self):
62+
with open(METADATA_QUERY_ERROR, 'rb') as f:
63+
response_json = json.load(f)
64+
with requests_mock.mock() as m:
65+
m.post(self.baseurl, json=response_json)
66+
67+
with self.assertRaises(GraphQLError) as e:
68+
self.server.metadata.query('fake query', abort_on_error=True)
69+
self.assertListEqual(e.error, EXPECTED_DICT_ERROR)

0 commit comments

Comments
 (0)
0