Skip to content
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
1 change: 1 addition & 0 deletions tableauserverclient/server/endpoint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .exceptions import ServerResponseError, MissingRequiredFieldError, ServerInfoEndpointNotFoundError
from .groups_endpoint import Groups
from .jobs_endpoint import Jobs
from .metadata_endpoint import Metadata
from .projects_endpoint import Projects
from .schedules_endpoint import Schedules
from .server_info_endpoint import ServerInfo
Expand Down
9 changes: 9 additions & 0 deletions tableauserverclient/server/endpoint/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,12 @@ class EndpointUnavailableError(Exception):

class ItemTypeNotAllowed(Exception):
pass


class GraphQLError(Exception):
def __init__(self, error_payload):
self.error = error_payload

def __str__(self):
from pprint import pformat
return pformat(self.error)
32 changes: 32 additions & 0 deletions tableauserverclient/server/endpoint/metadata_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from .endpoint import Endpoint, api
from .exceptions import GraphQLError
import logging
import json

logger = logging.getLogger('tableau.endpoint.metadata')


class Metadata(Endpoint):
@property
def baseurl(self):
return "{0}/api/exp/metadata/graphql".format(self.parent_srv._server_address)

@api("3.2")
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@gaoang2148 what's the latest, is it 3.4?

Copy link
Contributor

Choose a reason for hiding this comment

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

3.4 is 19.2, 3.5 is 19.3

Copy link
Contributor

Choose a reason for hiding this comment

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

This also jumps out to me, minor issue so just merged!

def query(self, query, abort_on_error=False):
logger.info('Querying Metadata API')
url = self.baseurl

try:
graphql_query = json.dumps({'query': query})
except Exception:
# Place holder for now
raise Exception('Must provide a string')

# Setting content type because post_reuqest defaults to text/xml
server_response = self.post_request(url, graphql_query, content_type='text/json')
results = server_response.json()

if abort_on_error and results.get('errors', None):
raise GraphQLError(results['errors'])

return results
5 changes: 3 additions & 2 deletions tableauserverclient/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from .exceptions import NotSignedInError
from ..namespace import Namespace
from .endpoint import Sites, Views, Users, Groups, Workbooks, Datasources, Projects, Auth, \
Schedules, ServerInfo, Tasks, ServerInfoEndpointNotFoundError, Subscriptions, Jobs
from .endpoint.exceptions import EndpointUnavailableError
Schedules, ServerInfo, Tasks, ServerInfoEndpointNotFoundError, Subscriptions, Jobs, Metadata
from .endpoint.exceptions import EndpointUnavailableError, ServerInfoEndpointNotFoundError

import requests

Expand Down Expand Up @@ -50,6 +50,7 @@ def __init__(self, server_address, use_server_version=False):
self.server_info = ServerInfo(self)
self.tasks = Tasks(self)
self.subscriptions = Subscriptions(self)
self.metadata = Metadata(self)
self._namespace = Namespace()

if use_server_version:
Expand Down
29 changes: 29 additions & 0 deletions test/assets/metadata_query_error.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"data": {
"publishedDatasources": [
{
"id": "01cf92b2-2d17-b656-fc48-5c25ef6d5352",
"name": "Batters (TestV1)"
},
{
"id": "020ae1cd-c356-f1ad-a846-b0094850d22a",
"name": "SharePoint_List_sharepoint2010.test.tsi.lan"
},
{
"id": "061493a0-c3b2-6f39-d08c-bc3f842b44af",
"name": "Batters_mongodb"
},
{
"id": "089fe515-ad2f-89bc-94bd-69f55f69a9c2",
"name": "Sample - Superstore"
}
]
},
"errors": [
{
"message": "Reached time limit of PT5S for query execution.",
"path": null,
"extensions": null
}
]
}
22 changes: 22 additions & 0 deletions test/assets/metadata_query_success.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"data": {
"publishedDatasources": [
{
"id": "01cf92b2-2d17-b656-fc48-5c25ef6d5352",
"name": "Batters (TestV1)"
},
{
"id": "020ae1cd-c356-f1ad-a846-b0094850d22a",
"name": "SharePoint_List_sharepoint2010.test.tsi.lan"
},
{
"id": "061493a0-c3b2-6f39-d08c-bc3f842b44af",
"name": "Batters_mongodb"
},
{
"id": "089fe515-ad2f-89bc-94bd-69f55f69a9c2",
"name": "Sample - Superstore"
}
]
}
}
69 changes: 69 additions & 0 deletions test/test_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import unittest
import os.path
import json
import requests_mock
import tableauserverclient as TSC

from tableauserverclient.server.endpoint.exceptions import GraphQLError

TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets')

METADATA_QUERY_SUCCESS = os.path.join(TEST_ASSET_DIR, 'metadata_query_success.json')
METADATA_QUERY_ERROR = os.path.join(TEST_ASSET_DIR, 'metadata_query_error.json')

EXPECTED_DICT = {'publishedDatasources':
[{'id': '01cf92b2-2d17-b656-fc48-5c25ef6d5352', 'name': 'Batters (TestV1)'},
{'id': '020ae1cd-c356-f1ad-a846-b0094850d22a', 'name': 'SharePoint_List_sharepoint2010.test.tsi.lan'},
{'id': '061493a0-c3b2-6f39-d08c-bc3f842b44af', 'name': 'Batters_mongodb'},
{'id': '089fe515-ad2f-89bc-94bd-69f55f69a9c2', 'name': 'Sample - Superstore'}]}

EXPECTED_DICT_ERROR = [
{
"message": "Reached time limit of PT5S for query execution.",
"path": None,
"extensions": None
}
]


class MetadataTests(unittest.TestCase):
def setUp(self):
self.server = TSC.Server('http://test')
self.baseurl = self.server.metadata.baseurl
self.server.version = "3.2"

self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67'
self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM'

def test_metadata_query(self):
with open(METADATA_QUERY_SUCCESS, 'rb') as f:
response_json = json.loads(f.read().decode())
with requests_mock.mock() as m:
m.post(self.baseurl, json=response_json)
actual = self.server.metadata.query('fake query')

datasources = actual['data']

self.assertDictEqual(EXPECTED_DICT, datasources)

def test_metadata_query_ignore_error(self):
with open(METADATA_QUERY_ERROR, 'rb') as f:
response_json = json.loads(f.read().decode())
with requests_mock.mock() as m:
m.post(self.baseurl, json=response_json)
actual = self.server.metadata.query('fake query')
datasources = actual['data']

self.assertNotEqual(actual.get('errors', None), None)
self.assertListEqual(EXPECTED_DICT_ERROR, actual['errors'])
self.assertDictEqual(EXPECTED_DICT, datasources)

def test_metadata_query_abort_on_error(self):
with open(METADATA_QUERY_ERROR, 'rb') as f:
response_json = json.loads(f.read().decode())
with requests_mock.mock() as m:
m.post(self.baseurl, json=response_json)

with self.assertRaises(GraphQLError) as e:
self.server.metadata.query('fake query', abort_on_error=True)
self.assertListEqual(e.error, EXPECTED_DICT_ERROR)