Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
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
6 changes: 5 additions & 1 deletion 6 CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
## 0.5 (11 Sept 2017)
## 0.5.1 (21 Sept 2017

* Fix a critical issue caused by #224 that was the result of lack of test coverage (#226)

## 0.5 (20 Sept 2017)

* Added revision settings to update site (#187)
* Added support for certified data sources (#189)
Expand Down
85 changes: 85 additions & 0 deletions 85 samples/set_refresh_schedule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import argparse
import getpass
import logging

import tableauserverclient as TSC


def usage(args):
parser = argparse.ArgumentParser(description='Explore workbook 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('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error',
help='desired logging level (set to error by default)')
parser.add_argument('--password', '-p', default=None)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--workbook', '-w')
group.add_argument('--datasource', '-d')
parser.add_argument('schedule')

return parser.parse_args(args)


def make_filter(**kwargs):
options = TSC.RequestOptions()
for item, value in kwargs.items():
name = getattr(TSC.RequestOptions.Field, item)
options.filter.add(TSC.Filter(name, TSC.RequestOptions.Operator.Equals, value))
return options


def get_workbook_by_name(server, name):
request_filter = make_filter(Name=name)
workbooks, _ = server.workbooks.get(request_filter)
assert len(workbooks) == 1
return workbooks.pop()


def get_datasource_by_name(server, name):
request_filter = make_filter(Name=name)
datasources, _ = server.datasources.get(request_filter)
assert len(datasources) == 1
return datasources.pop()


def get_schedule_by_name(server, name):
schedules = [x for x in TSC.Pager(server.schedules) if x.name == name]
assert len(schedules) == 1
return schedules.pop()


def assign_to_schedule(server, workbook_or_datasource, schedule):
server.schedules.add_to_schedule(schedule.id, workbook_or_datasource)


def run(args):
password = args.password
if password is None:
password = getpass.getpass("Password: ")

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

# Step 1: Sign in to server.
tableau_auth = TSC.TableauAuth(args.username, password)
server = TSC.Server(args.server, use_server_version=True)

with server.auth.sign_in(tableau_auth):
if args.workbook:
item = get_workbook_by_name(server, args.workbook)
else:
item = get_datasource_by_name(server, args.datasource)
schedule = get_schedule_by_name(server, args.schedule)

assign_to_schedule(server, item, schedule)


def main():
import sys
args = usage(sys.argv[1:])
run(args)


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion 2 tableauserverclient/server/endpoint/endpoint.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .exceptions import ServerResponseError, EndpointUnavailableError
from .exceptions import ServerResponseError, EndpointUnavailableError, ItemTypeNotAllowed
from functools import wraps

import logging
Expand Down
4 changes: 4 additions & 0 deletions 4 tableauserverclient/server/endpoint/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ class ServerInfoEndpointNotFoundError(Exception):

class EndpointUnavailableError(Exception):
pass


class ItemTypeNotAllowed(Exception):
pass
35 changes: 34 additions & 1 deletion 35 tableauserverclient/server/endpoint/schedules_endpoint.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
from .endpoint import Endpoint, api
from .exceptions import MissingRequiredFieldError
from .. import RequestFactory, PaginationItem, ScheduleItem
from .. import RequestFactory, PaginationItem, ScheduleItem, WorkbookItem, DatasourceItem
import logging
import copy
from collections import namedtuple

logger = logging.getLogger('tableau.endpoint.schedules')
# Oh to have a first class Result concept in Python...
AddResponse = namedtuple('AddResponse', ('result', 'error'))
OK = AddResponse(result=True, error=None)


class Schedules(Endpoint):
@property
def baseurl(self):
return "{0}/schedules".format(self.parent_srv.baseurl)

@property
def siteurl(self):
return "{0}/sites/{1}/schedules".format(self.parent_srv.baseurl, self.parent_srv.site_id)

@api(version="2.3")
def get(self, req_options=None):
logger.info("Querying all schedules")
Expand Down Expand Up @@ -58,3 +66,28 @@ def create(self, schedule_item):
new_schedule = ScheduleItem.from_response(server_response.content, self.parent_srv.namespace)[0]
logger.info("Created new schedule (ID: {})".format(new_schedule.id))
return new_schedule

@api(version="2.8")
def add_to_schedule(self, schedule_id, workbook=None, datasource=None):

def add_to(resource, type_, req_factory):
id_ = resource.id
url = "{0}/{1}/{2}s".format(self.siteurl, schedule_id, type_)
add_req = req_factory(id_)
response = self.put_request(url, add_req)
if response.status_code < 200 or response.status_code >= 300:
return AddResponse(result=False,
error="Status {}: {}".format(response.status_code, response.reason))
logger.info("Added {} to {} to schedule {}".format(type_, id_, schedule_id))
return OK

items = []

if workbook is not None:
items.append((workbook, "workbook", RequestFactory.Schedule.add_workbook_req))
if datasource is not None:
items.append((datasource, "datasource", RequestFactory.Schedule.add_datasource_req))

results = (add_to(*x) for x in items)
# list() is needed for python 3.x compatibility
return list(filter(lambda x: not x.result, results))
Copy link
Collaborator

@t8y8 t8y8 Jan 16, 2018

Choose a reason for hiding this comment

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

EDIT: I figured it out.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

23 changes: 23 additions & 0 deletions 23 tableauserverclient/server/request_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,29 @@ def update_req(self, schedule_item):
single_interval_element.attrib[expression] = value
return ET.tostring(xml_request)

def _add_to_req(self, id_, type_):
"""
<task>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Man we should have done this for all the request factories, it's really nice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah... hindsight...

<extractRefresh>
<workbook/datasource id="..."/>
</extractRefresh>
</task>

"""
xml_request = ET.Element('tsRequest')
task_element = ET.SubElement(xml_request, 'task')
refresh = ET.SubElement(task_element, 'extractRefresh')
workbook = ET.SubElement(refresh, type_)
workbook.attrib['id'] = id_

return ET.tostring(xml_request)

def add_workbook_req(self, id_):
return self._add_to_req(id_, "workbook")

def add_datasource_req(self, id_):
return self._add_to_req(id_, "datasource")


class SiteRequest(object):
def update_req(self, site_item):
Expand Down
31 changes: 31 additions & 0 deletions 31 test/test_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
CREATE_MONTHLY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_monthly.xml")
UPDATE_XML = os.path.join(TEST_ASSET_DIR, "schedule_update.xml")

WORKBOOK_GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, 'workbook_get_by_id.xml')
DATASOURCE_GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, 'datasource_get_by_id.xml')


class ScheduleTests(unittest.TestCase):
def setUp(self):
Expand Down Expand Up @@ -182,3 +185,31 @@ def test_update(self):
self.assertEqual(time(7), single_schedule.interval_item.start_time)
self.assertEqual(("Monday", "Friday"),
single_schedule.interval_item.interval)

def test_add_workbook(self):
self.server.version = "2.8"
baseurl = "{}/sites/{}/schedules".format(self.server.baseurl, self.server.site_id)

with open(WORKBOOK_GET_BY_ID_XML, "rb") as f:
workbook_response = f.read().decode("utf-8")
with requests_mock.mock() as m:
# TODO: Replace with real response
m.get(self.server.workbooks.baseurl + '/bar', text=workbook_response)
m.put(baseurl + '/foo/workbooks', text="OK")
workbook = self.server.workbooks.get_by_id("bar")
result = self.server.schedules.add_to_schedule('foo', workbook=workbook)
self.assertEquals(0, len(result), "Added properly")

def test_add_datasource(self):
self.server.version = "2.8"
baseurl = "{}/sites/{}/schedules".format(self.server.baseurl, self.server.site_id)

with open(DATASOURCE_GET_BY_ID_XML, "rb") as f:
datasource_response = f.read().decode("utf-8")
with requests_mock.mock() as m:
# TODO: Replace with real response
m.get(self.server.datasources.baseurl + '/bar', text=datasource_response)
m.put(baseurl + '/foo/datasources', text="OK")
datasource = self.server.datasources.get_by_id("bar")
result = self.server.schedules.add_to_schedule('foo', datasource=datasource)
self.assertEquals(0, len(result), "Added properly")
Morty Proxy This is a proxified and sanitized view of the page, visit original site.