Skip to content
3 changes: 3 additions & 0 deletions docs/docs/api-ref.md
Original file line number Diff line number Diff line change
Expand Up @@ -2405,11 +2405,14 @@ Source file: models/view_item.py

Name | Description
:--- | :---
`created_at` | The date and time when the view was created.
`id` | The identifier of the view item.
`name` | The name of the view.
`owner_id` | The id for the owner of the view.
`preview_image` | The thumbnail image for the view.
`sheet_type` | The type of the view which is either a worksheet, a dashboard or a story.
`total_views` | The usage statistics for the view. Indicates the total number of times the view has been looked at.
`updated_at` | The date and time when the view was last updated.
`workbook_id` | The id of the workbook associated with the view.


Expand Down
52 changes: 52 additions & 0 deletions samples/login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
####
# This script demonstrates how to log in to Tableau Server Client.
#
# To run the script, you must have installed Python 2.7.9 or later.
####

import argparse
import getpass
import logging

import tableauserverclient as TSC


def main():
parser = argparse.ArgumentParser(description='Logs in to the server.')

parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error',
help='desired logging level (set to error by default)')

parser.add_argument('--server', '-s', required=True, help='server address')

group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--username', '-u', help='username to sign into the server')
group.add_argument('--token-name', '-n', help='name of the personal access token used to sign into the server')

args = parser.parse_args()

# Set logging level based on user input, or error by default.
logging_level = getattr(logging, args.logging_level.upper())
logging.basicConfig(level=logging_level)

# Make sure we use an updated version of the rest apis.
server = TSC.Server(args.server, use_server_version=True)

if args.username:
# Trying to authenticate using username and password.
password = getpass.getpass("Password: ")
tableau_auth = TSC.TableauAuth(args.username, password)
with server.auth.sign_in(tableau_auth):
print('Logged in successfully')

else:
# Trying to authenticate using personal access tokens.
personal_access_token = getpass.getpass("Personal Access Token: ")
tableau_auth = TSC.PersonalAccessTokenAuth(token_name=args.token_name,
personal_access_token=personal_access_token)
with server.auth.sign_in_with_personal_access_token(tableau_auth):
print('Logged in successfully')


if __name__ == '__main__':
main()
14 changes: 10 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import sys
import versioneer

try:
from setuptools import setup
except ImportError:
from distutils.core import setup

# Only install pytest and runner when test command is run
# This makes work easier for offline installs or low bandwidth machines
needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv)
pytest_runner = ['pytest-runner'] if needs_pytest else []

setup(
name='tableauserverclient',
version=versioneer.get_version(),
Expand All @@ -16,11 +23,10 @@
license='MIT',
description='A Python module for working with the Tableau Server REST API.',
test_suite='test',
setup_requires=[
'pytest-runner'
],
setup_requires=pytest_runner,
install_requires=[
'requests>=2.11,<3.0'
'requests>=2.11,<3.0',
'urllib3==1.24.3'
],
tests_require=[
'requests-mock>=1.0,<2.0',
Expand Down
4 changes: 2 additions & 2 deletions tableauserverclient/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from .namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE
from .models import ConnectionCredentials, ConnectionItem, DatasourceItem,\
GroupItem, JobItem, BackgroundJobItem, PaginationItem, ProjectItem, ScheduleItem, \
SiteItem, TableauAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \
SiteItem, TableauAuth, PersonalAccessTokenAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \
HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem, \
SubscriptionItem, Target
SubscriptionItem, Target, PermissionsRule, Permission
from .server import RequestOptions, CSVRequestOptions, ImageRequestOptions, PDFRequestOptions, Filter, Sort, \
Server, ServerResponseError, MissingRequiredFieldError, NotSignedInError, Pager
from ._version import get_versions
Expand Down
2 changes: 2 additions & 0 deletions tableauserverclient/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
from .server_info_item import ServerInfoItem
from .site_item import SiteItem
from .tableau_auth import TableauAuth
from .personal_access_token_auth import PersonalAccessTokenAuth
from .target import Target
from .task_item import TaskItem
from .user_item import UserItem
from .view_item import ViewItem
from .workbook_item import WorkbookItem
from .subscription_item import SubscriptionItem
from .permissions_item import PermissionsRule, Permission
12 changes: 12 additions & 0 deletions tableauserverclient/models/datasource_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,22 @@ def __init__(self, project_id, name=None):
self.project_id = project_id
self.tags = set()

self._permissions = None

@property
def connections(self):
if self._connections is None:
error = 'Datasource item must be populated with connections first.'
raise UnpopulatedPropertyError(error)
return self._connections()

@property
def permissions(self):
if self._permissions is None:
error = "Project item must be populated with permissions first."
raise UnpopulatedPropertyError(error)
return self._permissions()

@property
def content_url(self):
return self._content_url
Expand Down Expand Up @@ -84,6 +93,9 @@ def updated_at(self):
def _set_connections(self, connections):
self._connections = connections

def _set_permissions(self, permissions):
self._permissions = permissions

def _parse_common_elements(self, datasource_xml, ns):
if not isinstance(datasource_xml, ET.Element):
datasource_xml = ET.fromstring(datasource_xml).find('.//t:datasource', namespaces=ns)
Expand Down
4 changes: 4 additions & 0 deletions tableauserverclient/models/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
class UnpopulatedPropertyError(Exception):
pass


class UnknownGranteeTypeError(Exception):
pass
13 changes: 12 additions & 1 deletion tableauserverclient/models/group_item.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import xml.etree.ElementTree as ET
from .exceptions import UnpopulatedPropertyError
from .property_decorators import property_not_empty
from .reference_item import ResourceReference


class GroupItem(object):
def __init__(self, name):

tag_name = 'group'

def __init__(self, name=None):
self._domain_name = None
self._id = None
self._users = None
Expand Down Expand Up @@ -35,6 +39,9 @@ def users(self):
# Each call to `.users` should create a new pager, this just runs the callable
return self._users()

def to_reference(self):
return ResourceReference(id_=self.id, tag_name=self.tag_name)

def _set_users(self, users):
self._users = users

Expand All @@ -53,3 +60,7 @@ def from_response(cls, resp, ns):
group_item._domain_name = domain_elem.get('name', None)
all_group_items.append(group_item)
return all_group_items

@staticmethod
def as_reference(id_):
return ResourceReference(id_, GroupItem.tag_name)
93 changes: 93 additions & 0 deletions tableauserverclient/models/permissions_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import xml.etree.ElementTree as ET
import logging

from .exceptions import UnknownGranteeTypeError
from .user_item import UserItem
from .group_item import GroupItem

logger = logging.getLogger('tableau.models.permissions_item')


class Permission:

class Mode:
Allow = 'Allow'
Deny = 'Deny'

class Capability:
AddComment = 'AddComment'
ChangeHierarchy = 'ChangeHierarchy'
ChangePermissions = 'ChangePermissions'
Connect = 'Connect'
Delete = 'Delete'
ExportData = 'ExportData'
ExportImage = 'ExportImage'
ExportXml = 'ExportXml'
Filter = 'Filter'
ProjectLeader = 'ProjectLeader'
Read = 'Read'
ShareView = 'ShareView'
ViewComments = 'ViewComments'
ViewUnderlyingData = 'ViewUnderlyingData'
WebAuthoring = 'WebAuthoring'
Write = 'Write'

class Resource:
Workbook = 'workbook'
Datasource = 'datasource'
Flow = 'flow'


class PermissionsRule(object):

def __init__(self, grantee, capabilities):
self.grantee = grantee
self.capabilities = capabilities

@classmethod
def from_response(cls, resp, ns=None):
parsed_response = ET.fromstring(resp)

rules = []
permissions_rules_list_xml = parsed_response.findall('.//t:granteeCapabilities',
namespaces=ns)

for grantee_capability_xml in permissions_rules_list_xml:
capability_dict = {}

grantee = PermissionsRule._parse_grantee_element(grantee_capability_xml, ns)

for capability_xml in grantee_capability_xml.findall(
'.//t:capabilities/t:capability', namespaces=ns):
name = capability_xml.get('name')
mode = capability_xml.get('mode')

capability_dict[name] = mode

rule = PermissionsRule(grantee,
capability_dict)
rules.append(rule)

return rules

@staticmethod
def _parse_grantee_element(grantee_capability_xml, ns):
"""Use Xpath magic and some string splitting to get the right object type from the xml"""

# Get the first element in the tree with an 'id' attribute
grantee_element = grantee_capability_xml.findall('.//*[@id]', namespaces=ns).pop()
grantee_id = grantee_element.get('id', None)
grantee_type = grantee_element.tag.split('}').pop()

if grantee_id is None:
logger.error('Cannot find grantee type in response')
raise UnknownGranteeTypeError()

if grantee_type == 'user':
grantee = UserItem.as_reference(grantee_id)
elif grantee_type == 'group':
grantee = GroupItem.as_reference(grantee_id)
else:
raise UnknownGranteeTypeError("No support for grantee type of {}".format(grantee_type))

return grantee
11 changes: 11 additions & 0 deletions tableauserverclient/models/personal_access_token_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class PersonalAccessTokenAuth(object):
def __init__(self, token_name, personal_access_token, site_id=''):
self.token_name = token_name
self.personal_access_token = personal_access_token
self.site_id = site_id
# Personal Access Tokens doesn't support impersonation.
self.user_id_to_impersonate = None

@property
def credentials(self):
return {'clientId': self.token_name, 'personalAccessToken': self.personal_access_token}
43 changes: 43 additions & 0 deletions tableauserverclient/models/project_item.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import xml.etree.ElementTree as ET

from .permissions_item import Permission

from .property_decorators import property_is_enum, property_not_empty
from .exceptions import UnpopulatedPropertyError


class ProjectItem(object):
Expand All @@ -15,10 +19,43 @@ def __init__(self, name, description=None, content_permissions=None, parent_id=N
self.content_permissions = content_permissions
self.parent_id = parent_id

self._permissions = None
self._default_workbook_permissions = None
self._default_datasource_permissions = None
self._default_flow_permissions = None

@property
def content_permissions(self):
return self._content_permissions

@property
def permissions(self):
if self._permissions is None:
error = "Project item must be populated with permissions first."
raise UnpopulatedPropertyError(error)
return self._permissions()

@property
def default_datasource_permissions(self):
if self._default_datasource_permissions is None:
error = "Project item must be populated with permissions first."
raise UnpopulatedPropertyError(error)
return self._default_datasource_permissions()

@property
def default_workbook_permissions(self):
if self._default_workbook_permissions is None:
error = "Project item must be populated with permissions first."
raise UnpopulatedPropertyError(error)
return self._default_workbook_permissions()

@property
def default_flow_permissions(self):
if self._default_flow_permissions is None:
error = "Project item must be populated with permissions first."
raise UnpopulatedPropertyError(error)
return self._default_flow_permissions()

@content_permissions.setter
@property_is_enum(ContentPermissions)
def content_permissions(self, value):
Expand Down Expand Up @@ -61,6 +98,12 @@ def _set_values(self, project_id, name, description, content_permissions, parent
if parent_id:
self.parent_id = parent_id

def _set_permissions(self, permissions):
self._permissions = permissions

def _set_default_permissions(self, permissions, content_type):
setattr(self, "_default_{content}_permissions".format(content=content_type), permissions)

@classmethod
def from_response(cls, resp, ns):
all_project_items = list()
Expand Down
21 changes: 21 additions & 0 deletions tableauserverclient/models/reference_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class ResourceReference(object):

def __init__(self, id_, tag_name):
self.id = id_
self.tag_name = tag_name

@property
def id(self):
return self._id

@id.setter
def id(self, value):
self._id = value

@property
def tag_name(self):
return self._tag_name

@tag_name.setter
def tag_name(self, value):
self._tag_name = value
Loading