Skip to content
Closed
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: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
dist: xenial
language: python
python:
- "2.7"
- "3.5"
- "3.6"
- "3.7"
Expand Down
11 changes: 11 additions & 0 deletions contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,14 @@ creating a PR can be found in the [Development Guide](https://tableau.github.io/
If the feature is complex or has multiple solutions that could be equally appropriate approaches, it would be helpful to file an issue to discuss the
design trade-offs of each solution before implementing, to allow us to collectively arrive at the best solution, which most likely exists in the middle
somewhere.


## Getting Started
> pip install versioneer
> python setup.py build
> python setup.py test
>

### before committing
Our CI runs include a python lint run, so you should run this locally and fix complaints before committing as this will fail your checkin
> pycodestyle tableauserverclient test samples
82 changes: 82 additions & 0 deletions samples/explore_webhooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
####
# This script demonstrates how to use the Tableau Server Client
# to interact with webhooks. It explores the different
# functions that the Server API supports on webhooks.
#
# With no flags set, this sample will query all webhooks,
# pick one webhook and print the name of the webhook.
# Adding flags will demonstrate the specific feature
# on top of the general operations.
####

import argparse
import getpass
import logging
import os.path

import tableauserverclient as TSC


def main():

parser = argparse.ArgumentParser(description='Explore webhook functions supported by the Server API.')
parser.add_argument('--server', '-s', required=True, help='server address')
parser.add_argument('--username', '-u', required=True, help='username to sign into server')
parser.add_argument('--site', '-S', default=None)
parser.add_argument('-p', default=None, help='password')
parser.add_argument('--create', '-c', help='create a webhook')
parser.add_argument('--delete', '-d', help='delete a webhook', action='store_true')
parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error',
help='desired logging level (set to error by default)')

args = parser.parse_args()
if args.p is None:
password = getpass.getpass("Password: ")
else:
password = args.p

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

# SIGN IN
tableau_auth = TSC.TableauAuth(args.username, password, args.site)
print("Signing in to " + args.server + " [" + args.site + "] as " + args.username)
server = TSC.Server(args.server)

# Set http options to disable verifying SSL
server.add_http_options({'verify': False})

server.use_server_version()

with server.auth.sign_in(tableau_auth):

# Create webhook if create flag is set (-create, -c)
if args.create:

new_webhook = TSC.WebhookItem()
new_webhook.name = args.create
new_webhook.url = "https://ifttt.com/maker-url"
new_webhook.event = "datasource-created"
print(new_webhook)
new_webhook = server.webhooks.create(new_webhook)
print("Webhook created. ID: {}".format(new_webhook.id))

# Gets all webhook items
all_webhooks, pagination_item = server.webhooks.get()
print("\nThere are {} webhooks on site: ".format(pagination_item.total_available))
print([webhook.name for webhook in all_webhooks])

if all_webhooks:
# Pick one webhook from the list and delete it
sample_webhook = all_webhooks[0]
# sample_webhook.delete()
print("+++"+sample_webhook.name)

if (args.delete):
print("Deleting webhook " + sample_webhook.name)
server.webhooks.delete(sample_webhook.id)


if __name__ == '__main__':
main()
22 changes: 12 additions & 10 deletions samples/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,36 @@
import argparse
import getpass
import logging
import os
import sys

import tableauserverclient as TSC


def main():
parser = argparse.ArgumentParser(description='List out the names and LUIDs for different resource types')
parser.add_argument('--server', '-s', required=True, help='server address')
parser.add_argument('--site', '-S', default=None, help='site to log into, do not specify for default site')
parser.add_argument('--username', '-u', required=True, help='username to sign into server')
parser.add_argument('--password', '-p', default=None, help='password for the user')
parser.add_argument('--site', '-S', default="", help='site to log into, do not specify for default site')
parser.add_argument('--token-name', '-n', required=True, help='username to signin under')
parser.add_argument('--token', '-t', help='personal access token for logging in')

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

parser.add_argument('resource_type', choices=['workbook', 'datasource', 'project', 'view', 'job'])
parser.add_argument('resource_type', choices=['workbook', 'datasource', 'project', 'view', 'job', 'webhooks'])

args = parser.parse_args()

if args.password is None:
password = getpass.getpass("Password: ")
else:
password = args.password
token = os.environ.get('TOKEN', args.token)
if not token:
print("--token or TOKEN environment variable needs to be set")
sys.exit(1)

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

# SIGN IN
tableau_auth = TSC.TableauAuth(args.username, password, args.site)
tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, token, site_id=args.site)
server = TSC.Server(args.server, use_server_version=True)
with server.auth.sign_in(tableau_auth):
endpoint = {
Expand All @@ -44,6 +45,7 @@ def main():
'view': server.views,
'job': server.jobs,
'project': server.projects,
'webhooks': server.webhooks,
}.get(args.resource_type)

for resource in TSC.Pager(endpoint.get):
Expand Down
25 changes: 9 additions & 16 deletions samples/move_workbook_sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def main():
workbook_path = source_server.workbooks.download(all_workbooks[0].id, tmpdir)

# Step 4: Check if destination site exists, then sign in to the site
pagination_info, all_sites = source_server.sites.get()
all_sites, pagination_info = source_server.sites.get()
found_destination_site = any((True for site in all_sites if
args.destination_site.lower() == site.content_url.lower()))
if not found_destination_site:
Expand All @@ -71,21 +71,14 @@ def main():
# because of the different auth token and site ID.
with dest_server.auth.sign_in(tableau_auth):

# Step 5: Find destination site's default project
pagination_info, dest_projects = dest_server.projects.get()
target_project = next((project for project in dest_projects if project.is_default()), None)

# Step 6: If default project is found, form a new workbook item and publish.
if target_project is not None:
new_workbook = TSC.WorkbookItem(name=args.workbook_name, project_id=target_project.id)
new_workbook = dest_server.workbooks.publish(new_workbook, workbook_path,
mode=TSC.Server.PublishMode.Overwrite)
print("Successfully moved {0} ({1})".format(new_workbook.name, new_workbook.id))
else:
error = "The default project could not be found."
raise LookupError(error)

# Step 7: Delete workbook from source site and delete temp directory
# Step 5: Create a new workbook item and publish workbook. Note that
# an empty project_id will publish to the 'Default' project.
new_workbook = TSC.WorkbookItem(name=args.workbook_name, project_id="")
new_workbook = dest_server.workbooks.publish(new_workbook, workbook_path,
mode=TSC.Server.PublishMode.Overwrite)
print("Successfully moved {0} ({1})".format(new_workbook.name, new_workbook.id))

# Step 6: Delete workbook from source site and delete temp directory
source_server.workbooks.delete(all_workbooks[0].id)

finally:
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
],
tests_require=[
'requests-mock>=1.0,<2.0',
'pytest'
'pytest',
'mock'
]
)
3 changes: 2 additions & 1 deletion tableauserverclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
GroupItem, JobItem, BackgroundJobItem, PaginationItem, ProjectItem, ScheduleItem,\
SiteItem, TableauAuth, PersonalAccessTokenAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError,\
HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem,\
SubscriptionItem, Target, PermissionsRule, Permission, DatabaseItem, TableItem, ColumnItem, FlowItem
SubscriptionItem, Target, PermissionsRule, Permission, DatabaseItem, TableItem, ColumnItem, FlowItem, \
WebhookItem, PersonalAccessTokenAuth
from .server import RequestOptions, CSVRequestOptions, ImageRequestOptions, PDFRequestOptions, Filter, Sort, \
Server, ServerResponseError, MissingRequiredFieldError, NotSignedInError, Pager
from ._version import get_versions
Expand Down
16 changes: 16 additions & 0 deletions tableauserverclient/filesys_helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import os
ALLOWED_SPECIAL = (' ', '.', '_', '-')


def to_filename(string_to_sanitize):
sanitized = (c for c in string_to_sanitize if c.isalnum() or c in ALLOWED_SPECIAL)
return "".join(sanitized)


def make_download_path(filepath, filename):
download_path = None

if filepath is None:
download_path = filename

elif os.path.isdir(filepath):
download_path = os.path.join(filepath, filename)

else:
download_path = filepath + os.path.splitext(filename)[1]

return download_path
2 changes: 2 additions & 0 deletions tableauserverclient/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@
from .workbook_item import WorkbookItem
from .subscription_item import SubscriptionItem
from .permissions_item import PermissionsRule, Permission
from .webhook_item import WebhookItem
from .personal_access_token_auth import PersonalAccessTokenAuth
9 changes: 9 additions & 0 deletions tableauserverclient/models/pagination_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,12 @@ def from_response(cls, resp, ns):
pagination_item._page_size = int(pagination_xml.get('pageSize', '-1'))
pagination_item._total_available = int(pagination_xml.get('totalAvailable', '-1'))
return pagination_item

@classmethod
def from_single_page_list(cls, l):
item = cls()
item._page_number = 1
item._page_size = len(l)
item._total_available = len(l)

return item
3 changes: 3 additions & 0 deletions tableauserverclient/models/personal_access_token_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ def __init__(self, token_name, personal_access_token, site_id=''):
@property
def credentials(self):
return {'personalAccessTokenName': self.token_name, 'personalAccessTokenSecret': self.personal_access_token}

def __repr__(self):
return "<PersonalAccessToken name={} token={}>".format(self.token_name, self.personal_access_token)
89 changes: 89 additions & 0 deletions tableauserverclient/models/webhook_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import xml.etree.ElementTree as ET
from .exceptions import UnpopulatedPropertyError
from .property_decorators import property_not_nullable, property_is_boolean, property_is_materialized_views_config
from .tag_item import TagItem
from .view_item import ViewItem
from .permissions_item import PermissionsRule
from ..datetime_helpers import parse_datetime
import re


NAMESPACE_RE = re.compile(r'^{.*}')


def _parse_event(events):
event = events[0]
# Strip out the namespace from the tag name
return NAMESPACE_RE.sub('', event.tag)


class WebhookItem(object):
def __init__(self):
self._id = None
self.name = None
self.url = None
self._event = None
self.owner_id = None

def _set_values(self, id, name, url, event, owner_id):
if id is not None:
self._id = id
if name:
self.name = name
if url:
self.url = url
if event:
self.event = event
if owner_id:
self.owner_id = owner_id

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

@property
def event(self):
if self._event:
return self._event.replace("webhook-source-event-", "")
return None

@event.setter
def event(self, value):
self._event = "webhook-source-event-{}".format(value)

@classmethod
def from_response(cls, resp, ns):
all_webhooks_items = list()
parsed_response = ET.fromstring(resp)
all_webhooks_xml = parsed_response.findall('.//t:webhook', namespaces=ns)
for webhook_xml in all_webhooks_xml:
values = cls._parse_element(webhook_xml, ns)

webhook_item = cls()
webhook_item._set_values(*values)
all_webhooks_items.append(webhook_item)
return all_webhooks_items

@staticmethod
def _parse_element(webhook_xml, ns):
id = webhook_xml.get('id', None)
name = webhook_xml.get('name', None)

url = None
url_tag = webhook_xml.find('.//t:webhook-destination-http', namespaces=ns)
if url_tag is not None:
url = url_tag.get('url', None)

event = webhook_xml.findall('.//t:webhook-source/*', namespaces=ns)
if event is not None and len(event) > 0:
event = _parse_event(event)

owner_id = None
owner_tag = webhook_xml.find('.//t:owner', namespaces=ns)
if owner_tag is not None:
owner_id = owner_tag.get('id', None)

return id, name, url, event, owner_id

def __repr__(self):
return "<Webhook id={} name={} url={} event={}>".format(self.id, self.name, self.url, self.event)
2 changes: 1 addition & 1 deletion tableauserverclient/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .. import ConnectionItem, DatasourceItem, DatabaseItem, JobItem, BackgroundJobItem, \
GroupItem, PaginationItem, ProjectItem, ScheduleItem, SiteItem, TableauAuth,\
UserItem, ViewItem, WorkbookItem, TableItem, TaskItem, SubscriptionItem, \
PermissionsRule, Permission, ColumnItem, FlowItem
PermissionsRule, Permission, ColumnItem, FlowItem, WebhookItem
from .endpoint import Auth, Datasources, Endpoint, Groups, Projects, Schedules, \
Sites, Tables, Users, Views, Workbooks, Subscriptions, ServerResponseError, \
MissingRequiredFieldError, Flows
Expand Down
3 changes: 2 additions & 1 deletion tableauserverclient/server/endpoint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
from .schedules_endpoint import Schedules
from .server_info_endpoint import ServerInfo
from .sites_endpoint import Sites
from .subscriptions_endpoint import Subscriptions
from .tables_endpoint import Tables
from .tasks_endpoint import Tasks
from .users_endpoint import Users
from .views_endpoint import Views
from .webhooks_endpoint import Webhooks
from .workbooks_endpoint import Workbooks
from .subscriptions_endpoint import Subscriptions
Loading