diff --git a/.gitignore b/.gitignore
index 251ef5d25..6e1779e11 100644
--- a/.gitignore
+++ b/.gitignore
@@ -66,6 +66,9 @@ docs/_build/
# PyBuilder
target/
+# PyCharm stuff
+.idea/
+
# IPython Notebook
.ipynb_checkpoints
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..d9aa404ed
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,14 @@
+## 0.2 (02 November 2016)
+
+* Added Initial Schedules Support (#48)
+* Added Initial Create Group endpoint (#69)
+* Added Connection Credentials for publishing datasources/workbooks (#80)
+* Added Pager object for handling pagination results and sample (#72, #90)
+* Added ServerInfo endpoint (#84)
+* Deprecated `site` as a parameter to `TableauAuth` in favor of `site_id`
+* Code Cleanup
+* Bugfixes
+
+## 0.1 (12 September 2016)
+
+* Initial Release to the world
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
new file mode 100644
index 000000000..c97e9301d
--- /dev/null
+++ b/CONTRIBUTORS.md
@@ -0,0 +1,16 @@
+This project wouldn't be possible without our amazing contributors.
+
+The following people have contributed to this project to make it possible, and we thank them for their contributions!
+
+## Contributors
+
+* [geordielad](https://github.com/geordielad)
+* [kovner](https://github.com/kovner)
+
+
+## Core Team
+
+* [shinchris](https://github.com/shinchris)
+* [lgraber](https://github.com/lgraber)
+* [t8y8](https://github.com/t8y8)
+* [RussTheAerialist](https://github.com/RussTheAerialist)
diff --git a/contributing.md b/contributing.md
new file mode 100644
index 000000000..b1eda5b55
--- /dev/null
+++ b/contributing.md
@@ -0,0 +1,55 @@
+# Contributing
+
+We welcome contributions to this project!
+
+Contribution can include, but are not limited to, any of the following:
+
+* File an Issue
+* Request a Feature
+* Implement a Requested Feature
+* Fix an Issue/Bug
+* Add/Fix documentation
+
+Contributions must follow the guidelines outlined on the [Tableau Organization](http://tableau.github.io/) page, though filing an issue or requesting
+a feature do not require the CLA.
+
+## Issues and Feature Requests
+
+To submit an issue/bug report, or to request a feature, please submit a [github issue](https://github.com/tableau/server-client-python/issues) to the repo.
+
+If you are submitting a bug report, please provide as much information as you can, including clear and concise repro steps, attaching any necessary
+files to assist in the repro. **Be sure to scrub the files of any potentially sensitive information. Issues are public.**
+
+For a feature request, please try to describe the scenario you are trying to accomplish that requires the feature. This will help us understand
+the limitations that you are running into, and provide us with a use case to know if we've satisfied your request.
+
+### Label usage on Issues
+
+The core team is responsible for assigning most labels to the issue. Labels
+are used for prioritizing the core team's work, and use the following
+definitions for labels.
+
+The following labels are only to be set or changed by the core team:
+
+* **bug** - A bug is an unintended behavior for existing functionality. It only relates to existing functionality and the behavior that is expected with that functionality. We do not use **bug** to indicate priority.
+* **enhancement** - An enhancement is a new piece of functionality and is related to the fact that new code will need to be written in order to close this issue. We do not use **enhancement** to indicate priority.
+* **CLARequired** - This label is used to indicate that the contribution will require that the CLA is signed before we can accept a PR. This label should not be used on Issues
+* **CLANotRequired** - This label is used to indicate that the contribution does not require a CLA to be signed. This is used for minor fixes and usually around doc fixes or correcting strings.
+* **help wanted** - This label on an issue indicates it's a good choice for external contributors to take on. It usually means it's an issue that can be tackled by first time contributors.
+
+The following labels can be used by the issue creator or anyone in the
+community to help us prioritize enhancement and bug fixes that are
+causing pain from our users. The short of it is, purple tags are ones that
+anyone can add to an issue:
+
+* **Critical** - This means that you won't be able to use the library until the issues have been resolved. If an issue is already labeled as critical, but you want to show your support for it, add a +1 comment to the issue. This helps us know what issues are really impacting our users.
+* **Nice To Have** - This means that the issue doesn't block your usage of the library, but would make your life easier. Like with critical, if the issue is already tagged with this, but you want to show your support, add a +1 comment to the issue.
+
+## Fixes, Implementations, and Documentation
+
+For all other things, please submit a PR that includes the fix, documentation, or new code that you are trying to contribute. More information on
+creating a PR can be found in the [github documentation](https://help.github.com/articles/creating-a-pull-request/)
+
+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.
diff --git a/samples/create_group.py b/samples/create_group.py
new file mode 100644
index 000000000..3b7892fdf
--- /dev/null
+++ b/samples/create_group.py
@@ -0,0 +1,42 @@
+####
+# This script demonstrates how to create groups using the Tableau
+# Server Client.
+#
+# To run the script, you must have installed Python 2.7.9 or later.
+####
+
+
+import argparse
+import getpass
+import logging
+
+from datetime import time
+
+import tableauserverclient as TSC
+
+
+def main():
+
+ parser = argparse.ArgumentParser(description='Creates sample schedules for each type of frequency.')
+ 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)')
+ args = parser.parse_args()
+
+ 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)
+
+ tableau_auth = TSC.TableauAuth(args.username, password)
+ server = TSC.Server(args.server)
+ with server.auth.sign_in(tableau_auth):
+ group = TSC.GroupItem('test')
+ group = server.groups.create(group)
+ print(group)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/samples/create_schedules.py b/samples/create_schedules.py
new file mode 100644
index 000000000..c8d32b087
--- /dev/null
+++ b/samples/create_schedules.py
@@ -0,0 +1,77 @@
+####
+# This script demonstrates how to create schedules using the Tableau
+# Server Client.
+#
+# To run the script, you must have installed Python 2.7.9 or later.
+####
+
+
+import argparse
+import getpass
+import logging
+
+from datetime import time
+
+import tableauserverclient as TSC
+
+
+def main():
+
+ parser = argparse.ArgumentParser(description='Creates sample schedules for each type of frequency.')
+ 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)')
+ args = parser.parse_args()
+
+ 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)
+
+ tableau_auth = TSC.TableauAuth(args.username, password)
+ server = TSC.Server(args.server)
+ with server.auth.sign_in(tableau_auth):
+ # Hourly Schedule
+ # This schedule will run every 2 hours between 2:30AM and 11:00PM
+ hourly_interval = TSC.HourlyInterval(start_time=time(2, 30),
+ end_time=time(23, 0),
+ interval_value=2)
+
+ hourly_schedule = TSC.ScheduleItem("Hourly-Schedule", 50, TSC.ScheduleItem.Type.Extract,
+ TSC.ScheduleItem.ExecutionOrder.Parallel, hourly_interval)
+ hourly_schedule = server.schedules.create(hourly_schedule)
+ print("Hourly schedule created (ID: {}).".format(hourly_schedule.id))
+
+ # Daily Schedule
+ # This schedule will run every day at 5AM
+ daily_interval = TSC.DailyInterval(start_time=time(5))
+ daily_schedule = TSC.ScheduleItem("Daily-Schedule", 60, TSC.ScheduleItem.Type.Subscription,
+ TSC.ScheduleItem.ExecutionOrder.Serial, daily_interval)
+ daily_schedule = server.schedules.create(daily_schedule)
+ print("Daily schedule created (ID: {}).".format(daily_schedule.id))
+
+ # Weekly Schedule
+ # This schedule will wun every Monday, Wednesday, and Friday at 7:15PM
+ weekly_interval = TSC.WeeklyInterval(time(19, 15),
+ TSC.IntervalItem.Day.Monday,
+ TSC.IntervalItem.Day.Wednesday,
+ TSC.IntervalItem.Day.Friday)
+ weekly_schedule = TSC.ScheduleItem("Weekly-Schedule", 70, TSC.ScheduleItem.Type.Extract,
+ TSC.ScheduleItem.ExecutionOrder.Serial, weekly_interval)
+ weekly_schedule = server.schedules.create(weekly_schedule)
+ print("Weekly schedule created (ID: {}).".format(weekly_schedule.id))
+
+ # Monthly Schedule
+ # This schedule will run on the 15th of every month at 11:30PM
+ monthly_interval = TSC.MonthlyInterval(start_time=time(23, 30),
+ interval_value=15)
+ monthly_schedule = TSC.ScheduleItem("Monthly-Schedule", 80, TSC.ScheduleItem.Type.Subscription,
+ TSC.ScheduleItem.ExecutionOrder.Parallel, monthly_interval)
+ monthly_schedule = server.schedules.create(monthly_schedule)
+ print("Monthly schedule created (ID: {}).".format(monthly_schedule.id))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/samples/explore_datasource.py b/samples/explore_datasource.py
index 434cadd3b..260742cd4 100644
--- a/samples/explore_datasource.py
+++ b/samples/explore_datasource.py
@@ -1,5 +1,5 @@
####
-# This script demonstrates how to use the Tableau Server API
+# This script demonstrates how to use the Tableau Server Client
# to interact with datasources. It explores the different
# functions that the Server API supports on datasources.
#
@@ -9,56 +9,64 @@
# on top of the general operations.
####
-
-import tableauserverclient as TSC
-import os.path
import argparse
import getpass
import logging
-parser = argparse.ArgumentParser(description='Explore datasource 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('--publish', '-p', metavar='FILEPATH', help='path to datasource to publish')
-parser.add_argument('--download', '-d', metavar='FILEPATH', help='path to save downloaded datasource')
-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()
+import tableauserverclient as TSC
+
+
+def main():
+
+ parser = argparse.ArgumentParser(description='Explore datasource 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('--publish', '-p', metavar='FILEPATH', help='path to datasource to publish')
+ parser.add_argument('--download', '-d', metavar='FILEPATH', help='path to save downloaded datasource')
+ 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()
+
+ 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)
-password = getpass.getpass("Password: ")
+ # SIGN IN
+ tableau_auth = TSC.TableauAuth(args.username, password)
+ server = TSC.Server(args.server)
+ with server.auth.sign_in(tableau_auth):
+ # Query projects for use when demonstrating publishing and updating
+ all_projects, pagination_item = server.projects.get()
+ default_project = next((project for project in all_projects if project.is_default()), None)
-# Set logging level based on user input, or error by default
-logging_level = getattr(logging, args.logging_level.upper())
-logging.basicConfig(level=logging_level)
+ # Publish datasource if publish flag is set (-publish, -p)
+ if args.publish:
+ if default_project is not None:
+ new_datasource = TSC.DatasourceItem(default_project.id)
+ new_datasource = server.datasources.publish(
+ new_datasource, args.publish, TSC.Server.PublishMode.Overwrite)
+ print("Datasource published. ID: {}".format(new_datasource.id))
+ else:
+ print("Publish failed. Could not find the default project.")
-# SIGN IN
-tableau_auth = TSC.TableauAuth(args.username, password)
-server = TSC.Server(args.server)
-with server.auth.sign_in(tableau_auth):
- # Query projects for use when demonstrating publishing and updating
- all_projects, pagination_item = server.projects.get()
- default_project = next((project for project in all_projects if project.is_default()), None)
+ # Gets all datasource items
+ all_datasources, pagination_item = server.datasources.get()
+ print("\nThere are {} datasources on site: ".format(pagination_item.total_available))
+ print([datasource.name for datasource in all_datasources])
- # Publish datasource if publish flag is set (-publish, -p)
- if args.publish:
- if default_project is not None:
- new_datasource = TSC.DatasourceItem(default_project.id)
- new_datasource = server.datasources.publish(new_datasource, args.publish, server.PublishMode.Overwrite)
- print("Datasource published. ID: {}".format(new_datasource.id))
- else:
- print("Publish failed. Could not find the default project.")
+ if all_datasources:
+ # Pick one datasource from the list
+ sample_datasource = all_datasources[0]
- # Gets all datasource items
- all_datasources, pagination_item = server.datasources.get()
- print("\nThere are {} datasources on site: ".format(pagination_item.total_available))
- print([datasource.name for datasource in all_datasources])
+ # Populate connections
+ server.datasources.populate_connections(sample_datasource)
+ print("\nConnections for {}: ".format(sample_datasource.name))
+ print(["{0}({1})".format(connection.id, connection.datasource_name)
+ for connection in sample_datasource.connections])
- if all_datasources:
- # Pick one datasource from the list
- sample_datasource = all_datasources[0]
- # Populate connections
- server.datasources.populate_connections(sample_datasource)
- print("\nConnections for {}: ".format(sample_datasource.name))
- print(["{0}({1})".format(connection.id, connection.datasource_name)
- for connection in sample_datasource.connections])
+if __name__ == '__main__':
+ main()
diff --git a/samples/explore_workbook.py b/samples/explore_workbook.py
index 93897388e..6cdb2b1a2 100644
--- a/samples/explore_workbook.py
+++ b/samples/explore_workbook.py
@@ -1,5 +1,5 @@
####
-# This script demonstrates how to use the Tableau Server API
+# This script demonstrates how to use the Tableau Server Client
# to interact with workbooks. It explores the different
# functions that the Server API supports on workbooks.
#
@@ -9,88 +9,99 @@
# on top of the general operations.
####
-import tableauserverclient as TSC
-import os.path
-import copy
import argparse
import getpass
import logging
+import os.path
+
+import tableauserverclient as TSC
+
+
+def main():
+
+ 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('--publish', '-p', metavar='FILEPATH', help='path to workbook to publish')
+ parser.add_argument('--download', '-d', metavar='FILEPATH', help='path to save downloaded workbook')
+ parser.add_argument('--preview-image', '-i', metavar='FILENAME',
+ help='filename (a .png file) to save the preview image')
+ 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()
+
+ 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)
+
+ # SIGN IN
+ tableau_auth = TSC.TableauAuth(args.username, password)
+ server = TSC.Server(args.server)
+
+ overwrite_true = TSC.Server.PublishMode.Overwrite
+
+ with server.auth.sign_in(tableau_auth):
+
+ # Publish workbook if publish flag is set (-publish, -p)
+ if args.publish:
+ all_projects, pagination_item = server.projects.get()
+ default_project = next((project for project in all_projects if project.is_default()), None)
+
+ if default_project is not None:
+ new_workbook = TSC.WorkbookItem(default_project.id)
+ new_workbook = server.workbooks.publish(new_workbook, args.publish, overwrite_true)
+ print("Workbook published. ID: {}".format(new_workbook.id))
+ else:
+ print('Publish failed. Could not find the default project.')
+
+ # Gets all workbook items
+ all_workbooks, pagination_item = server.workbooks.get()
+ print("\nThere are {} workbooks on site: ".format(pagination_item.total_available))
+ print([workbook.name for workbook in all_workbooks])
+
+ if all_workbooks:
+ # Pick one workbook from the list
+ sample_workbook = all_workbooks[0]
+
+ # Populate views
+ server.workbooks.populate_views(sample_workbook)
+ print("\nName of views in {}: ".format(sample_workbook.name))
+ print([view.name for view in sample_workbook.views])
+
+ # Populate connections
+ server.workbooks.populate_connections(sample_workbook)
+ print("\nConnections for {}: ".format(sample_workbook.name))
+ print(["{0}({1})".format(connection.id, connection.datasource_name)
+ for connection in sample_workbook.connections])
+
+ # Update tags and show_tabs flag
+ original_tag_set = set(sample_workbook.tags)
+ sample_workbook.tags.update('a', 'b', 'c', 'd')
+ sample_workbook.show_tabs = True
+ server.workbooks.update(sample_workbook)
+ print("\nOld tag set: {}".format(original_tag_set))
+ print("New tag set: {}".format(sample_workbook.tags))
+ print("Workbook tabbed: {}".format(sample_workbook.show_tabs))
+
+ # Delete all tags that were added by setting tags to original
+ sample_workbook.tags = original_tag_set
+ server.workbooks.update(sample_workbook)
+
+ if args.download:
+ # Download
+ path = server.workbooks.download(sample_workbook.id, args.download)
+ print("\nDownloaded workbook to {}".format(path))
+
+ if args.preview_image:
+ # Populate workbook preview image
+ server.workbooks.populate_preview_image(sample_workbook)
+ with open(args.preview_image, 'wb') as f:
+ f.write(sample_workbook.preview_image)
+ print("\nDownloaded preview image of workbook to {}".format(os.path.abspath(args.preview_image)))
+
-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('--publish', '-p', metavar='FILEPATH', help='path to workbook to publish')
-parser.add_argument('--download', '-d', metavar='FILEPATH', help='path to save downloaded workbook')
-parser.add_argument('--preview-image', '-i', metavar='FILENAME',
- help='filename (a .png file) to save the preview image')
-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()
-
-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)
-
-# SIGN IN
-tableau_auth = TSC.TableauAuth(args.username, password)
-server = TSC.Server(args.server)
-with server.auth.sign_in(tableau_auth):
-
- # Publish workbook if publish flag is set (-publish, -p)
- if args.publish:
- all_projects, pagination_item = server.projects.get()
- default_project = next((project for project in all_projects if project.is_default()), None)
-
- if default_project is not None:
- new_workbook = TSC.WorkbookItem(default_project.id)
- new_workbook = server.workbooks.publish(new_workbook, args.publish, server.PublishMode.Overwrite)
- print("Workbook published. ID: {}".format(new_workbook.id))
- else:
- print('Publish failed. Could not find the default project.')
-
- # Gets all workbook items
- all_workbooks, pagination_item = server.workbooks.get()
- print("\nThere are {} workbooks on site: ".format(pagination_item.total_available))
- print([workbook.name for workbook in all_workbooks])
-
- if all_workbooks:
- # Pick one workbook from the list
- sample_workbook = all_workbooks[0]
-
- # Populate views
- server.workbooks.populate_views(sample_workbook)
- print("\nName of views in {}: ".format(sample_workbook.name))
- print([view.name for view in sample_workbook.views])
-
- # Populate connections
- server.workbooks.populate_connections(sample_workbook)
- print("\nConnections for {}: ".format(sample_workbook.name))
- print(["{0}({1})".format(connection.id, connection.datasource_name)
- for connection in sample_workbook.connections])
-
- # Update tags and show_tabs flag
- original_tag_set = copy.copy(sample_workbook.tags)
- sample_workbook.tags.update('a', 'b', 'c', 'd')
- sample_workbook.show_tabs = True
- server.workbooks.update(sample_workbook)
- print("\nOld tag set: {}".format(original_tag_set))
- print("New tag set: {}".format(sample_workbook.tags))
- print("Workbook tabbed: {}".format(sample_workbook.show_tabs))
-
- # Delete all tags that were added by setting tags to original
- sample_workbook.tags = original_tag_set
- server.workbooks.update(sample_workbook)
-
- if args.download:
- # Download
- path = server.workbooks.download(sample_workbook.id, args.download)
- print("\nDownloaded workbook to {}".format(path))
-
- if args.preview_image:
- # Populate workbook preview image
- server.workbooks.populate_preview_image(sample_workbook)
- with open(args.preview_image, 'wb') as f:
- f.write(sample_workbook.preview_image)
- print("\nDownloaded preview image of workbook to {}".format(os.path.abspath(args.preview_image)))
+if __name__ == '__main__':
+ main()
diff --git a/samples/move_workbook_projects.py b/samples/move_workbook_projects.py
index 5e7835e72..8bb1b4e50 100644
--- a/samples/move_workbook_projects.py
+++ b/samples/move_workbook_projects.py
@@ -1,56 +1,66 @@
####
-# This script demonstrates how to use the Tableau Server API
+# This script demonstrates how to use the Tableau Server Client
# to move a workbook from one project to another. It will find
# a workbook that matches a given name and update it to be in
# the desired project.
#
-# To run the script, you must have installed Python 2.7.9 or later.
+# To run the script, you must have installed Python 2.7.X or 3.3 and later.
####
-import tableauserverclient as TSC
import argparse
import getpass
import logging
-parser = argparse.ArgumentParser(description='Move one workbook from the default project to another.')
-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('--workbook-name', '-w', required=True, help='name of workbook to move')
-parser.add_argument('--destination-project', '-d', required=True, help='name of project to move workbook into')
-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()
-
-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)
-with server.auth.sign_in(tableau_auth):
- # Step 2: Query workbook to move
- req_option = TSC.RequestOptions()
- req_option.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name,
- TSC.RequestOptions.Operator.Equals, args.workbook_name))
- all_workbooks, pagination_item = server.workbooks.get(req_option)
-
- # Step 3: Find destination project
- all_projects, pagination_item = server.projects.get()
- dest_project = next((project for project in all_projects if project.name == args.destination_project), None)
-
- if dest_project is not None:
- # Step 4: Update workbook with new project id
- if all_workbooks:
- print("Old project: {}".format(all_workbooks[0].project_name))
- all_workbooks[0].project_id = dest_project.id
- target_workbook = server.workbooks.update(all_workbooks[0])
- print("New project: {}".format(target_workbook.project_name))
+import tableauserverclient as TSC
+
+
+def main():
+
+ parser = argparse.ArgumentParser(description='Move one workbook from the default project to another.')
+ 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('--workbook-name', '-w', required=True, help='name of workbook to move')
+ parser.add_argument('--destination-project', '-d', required=True, help='name of project to move workbook into')
+ 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()
+
+ 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)
+
+ with server.auth.sign_in(tableau_auth):
+ # Step 2: Query workbook to move
+ req_option = TSC.RequestOptions()
+ req_option.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name,
+ TSC.RequestOptions.Operator.Equals, args.workbook_name))
+ all_workbooks, pagination_item = server.workbooks.get(req_option)
+
+ # Step 3: Find destination project
+ all_projects, pagination_item = server.projects.get()
+ dest_project = next((project for project in all_projects if project.name == args.destination_project), None)
+
+ if dest_project is not None:
+ # Step 4: Update workbook with new project id
+ if all_workbooks:
+ print("Old project: {}".format(all_workbooks[0].project_name))
+ all_workbooks[0].project_id = dest_project.id
+ target_workbook = server.workbooks.update(all_workbooks[0])
+ print("New project: {}".format(target_workbook.project_name))
+ else:
+ error = "No workbook named {} found.".format(args.workbook_name)
+ raise LookupError(error)
else:
- error = "No workbook named {} found.".format(args.workbook_name)
+ error = "No project named {} found.".format(args.destination_project)
raise LookupError(error)
- else:
- error = "No project named {} found.".format(args.destination_project)
- raise LookupError(error)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/samples/move_workbook_sites.py b/samples/move_workbook_sites.py
index 13103f82e..d81c96767 100644
--- a/samples/move_workbook_sites.py
+++ b/samples/move_workbook_sites.py
@@ -1,86 +1,96 @@
####
-# This script demonstrates how to use the Tableau Server API
+# This script demonstrates how to use the Tableau Server Client
# to move a workbook from one site to another. It will find
# a workbook that matches a given name, download the workbook,
# and then publish it to the destination site.
#
-# To run the script, you must have installed Python 2.7.9 or later.
+# To run the script, you must have installed Python 2.7.X or 3.3 and later.
####
-import tableauserverclient as TSC
-import shutil
import argparse
-import tempfile
import getpass
import logging
+import shutil
+import tempfile
+
+import tableauserverclient as TSC
+
+
+def main():
+
+ parser = argparse.ArgumentParser(description="Move one workbook from the"
+ "default project of the default site to"
+ "the default project of another site.")
+ 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('--workbook-name', '-w', required=True, help='name of workbook to move')
+ parser.add_argument('--destination-site', '-d', required=True, help='name of site to move workbook into')
+ 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()
+
+ password = getpass.getpass("Password: ")
-parser = argparse.ArgumentParser(description="Move one workbook from the"
- "default project of the default site to"
- "the default project of another site.")
-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('--workbook-name', '-w', required=True, help='name of workbook to move')
-parser.add_argument('--destination-site', '-d', required=True, help='name of site to move workbook into')
-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()
-
-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 both sites on server
-tableau_auth = TSC.TableauAuth(args.username, password)
-
-source_server = TSC.Server(args.server)
-dest_server = TSC.Server(args.server)
-
-with source_server.auth.sign_in(tableau_auth):
- # Step 2: Query workbook to move
- req_option = TSC.RequestOptions()
- req_option.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name,
- TSC.RequestOptions.Operator.Equals, args.workbook_name))
- all_workbooks, pagination_item = source_server.workbooks.get(req_option)
-
- # Step 3: Download workbook to a temp directory
- if len(all_workbooks) == 0:
- print('No workbook named {} found.'.format(args.workbook_name))
- else:
- tmpdir = tempfile.mkdtemp()
- try:
- 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()
- found_destination_site = any((True for site in all_sites if
- args.destination_site.lower() == site.content_url.lower()))
- if not found_destination_site:
- error = "No site named {} found.".format(args.destination_site)
- raise LookupError(error)
-
- tableau_auth.site = args.destination_site
-
- # Signing into another site requires another server object
- # 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=dest_server.PublishMode.Overwrite)
- print("Successfully moved {0} ({1})".format(new_workbook.name, new_workbook.id))
- else:
- error = "The default project could not be found."
+ # 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 both sites on server
+ tableau_auth = TSC.TableauAuth(args.username, password)
+
+ source_server = TSC.Server(args.server)
+ dest_server = TSC.Server(args.server)
+
+ with source_server.auth.sign_in(tableau_auth):
+ # Step 2: Query workbook to move
+ req_option = TSC.RequestOptions()
+ req_option.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name,
+ TSC.RequestOptions.Operator.Equals, args.workbook_name))
+ all_workbooks, pagination_item = source_server.workbooks.get(req_option)
+
+ # Step 3: Download workbook to a temp directory
+ if len(all_workbooks) == 0:
+ print('No workbook named {} found.'.format(args.workbook_name))
+ else:
+ tmpdir = tempfile.mkdtemp()
+ try:
+ 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()
+ found_destination_site = any((True for site in all_sites if
+ args.destination_site.lower() == site.content_url.lower()))
+ if not found_destination_site:
+ error = "No site named {} found.".format(args.destination_site)
raise LookupError(error)
- # Step 7: Delete workbook from source site and delete temp directory
- source_server.workbooks.delete(all_workbooks[0].id)
- finally:
- shutil.rmtree(tmpdir)
+ tableau_auth.site_id = args.destination_site
+
+ # Signing into another site requires another server object
+ # 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
+ source_server.workbooks.delete(all_workbooks[0].id)
+
+ finally:
+ shutil.rmtree(tmpdir)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/samples/pagination_sample.py b/samples/pagination_sample.py
new file mode 100644
index 000000000..882fc85ad
--- /dev/null
+++ b/samples/pagination_sample.py
@@ -0,0 +1,70 @@
+####
+# This script demonstrates how to use pagination item that is returned as part
+# of many of the .get() method calls.
+#
+# This script will iterate over every workbook that exists on the server using the
+# pagination item to fetch additional pages as needed.
+#
+# While this sample uses workbook, this same technique will work with any of the .get() methods that return
+# a pagination item
+####
+
+import argparse
+import getpass
+import logging
+import os.path
+
+import tableauserverclient as TSC
+
+
+def main():
+
+ parser = argparse.ArgumentParser(description='Return a list of all of the workbooks on your server')
+ 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)')
+
+ args = parser.parse_args()
+
+ 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)
+
+ # SIGN IN
+
+ tableau_auth = TSC.TableauAuth(args.username, password)
+ server = TSC.Server(args.server)
+
+ with server.auth.sign_in(tableau_auth):
+
+ # Pager returns a generator that yields one item at a time fetching
+ # from Server only when necessary. Pager takes a server Endpoint as its
+ # first parameter. It will call 'get' on that endpoint. To get workbooks
+ # pass `server.workbooks`, to get users pass` server.users`, etc
+ # You can then loop over the generator to get the objects one at a time
+ # Here we print the workbook id for each workbook
+
+ print("Your server contains the following workbooks:\n")
+ for wb in TSC.Pager(server.workbooks):
+ print(wb.name)
+
+ # Pager can also be used in list comprehensions or generator expressions
+ # for compactness and easy filtering. Generator expressions will use less
+ # memory than list comprehsnsions. Consult the Python laguage documentation for
+ # best practices on which are best for your use case. Here we loop over the
+ # Pager and only keep workbooks where the name starts with the letter 'a'
+ # >>> [wb for wb in TSC.Pager(server.workbooks) if wb.name.startswith('a')] # List Comprehension
+ # >>> (wb for wb in TSC.Pager(server.workbooks) if wb.name.startswith('a')) # Generator Expression
+
+ # Since Pager is a generator it follows the standard conventions and can
+ # be fed to a list if you really need all the workbooks in memory at once.
+ # If you need everything, it may be faster to use a larger page size
+
+ # >>> request_options = TSC.RequestOptions(pagesize=1000)
+ # >>> all_workbooks = list(TSC.Pager(server.workbooks, request_options))
+
+if __name__ == '__main__':
+ main()
diff --git a/samples/publish_workbook.py b/samples/publish_workbook.py
index 266cd1b2e..37d66d2dc 100644
--- a/samples/publish_workbook.py
+++ b/samples/publish_workbook.py
@@ -1,5 +1,5 @@
####
-# This script demonstrates how to use the Tableau Server API
+# This script demonstrates how to use the Tableau Server Client
# to publish a workbook to a Tableau server. It will publish
# a specified workbook to the 'default' project of the given server.
#
@@ -11,42 +11,54 @@
# For more information, refer to the documentations on 'Publish Workbook'
# (https://onlinehelp.tableau.com/current/api/rest_api/en-us/help.htm)
#
-# To run the script, you must have installed Python 2.7.9 or later.
+# To run the script, you must have installed Python 2.7.X or 3.3 and later.
####
-import tableauserverclient as TSC
import argparse
import getpass
import logging
-parser = argparse.ArgumentParser(description='Publish a workbook to server.')
-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('--filepath', '-f', required=True, help='filepath to the workbook to publish')
-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()
-
-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)
-with server.auth.sign_in(tableau_auth):
-
- # Step 2: Get all the projects on server, then look for the default one.
- all_projects, pagination_item = server.projects.get()
- default_project = next((project for project in all_projects if project.is_default()), None)
-
- # Step 3: If default project is found, form a new workbook item and publish.
- if default_project is not None:
- new_workbook = TSC.WorkbookItem(default_project.id)
- new_workbook = server.workbooks.publish(new_workbook, args.filepath, server.PublishMode.Overwrite)
- print("Workbook published. ID: {0}".format(new_workbook.id))
- else:
- error = "The default project could not be found."
- raise LookupError(error)
+import tableauserverclient as TSC
+
+
+def main():
+
+ parser = argparse.ArgumentParser(description='Publish a workbook to server.')
+ 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('--filepath', '-f', required=True, help='filepath to the workbook to publish')
+ 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()
+
+ 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)
+
+ overwrite_true = TSC.Server.PublishMode.Overwrite
+
+ with server.auth.sign_in(tableau_auth):
+
+ # Step 2: Get all the projects on server, then look for the default one.
+ all_projects, pagination_item = server.projects.get()
+ default_project = next((project for project in all_projects if project.is_default()), None)
+
+ # Step 3: If default project is found, form a new workbook item and publish.
+ if default_project is not None:
+ new_workbook = TSC.WorkbookItem(default_project.id)
+ new_workbook = server.workbooks.publish(new_workbook, args.filepath, overwrite_true)
+ print("Workbook published. ID: {0}".format(new_workbook.id))
+ else:
+ error = "The default project could not be found."
+ raise LookupError(error)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/samples/set_http_options.py b/samples/set_http_options.py
index 6700efb3e..fb5ce2441 100644
--- a/samples/set_http_options.py
+++ b/samples/set_http_options.py
@@ -2,41 +2,47 @@
# This script demonstrates how to set http options. It will set the option
# to not verify SSL certificate, and query all workbooks on site.
#
-# For more information, refer to the documentation on 'Publish Workbook'
-# (https://onlinehelp.tableau.com/current/api/rest_api/en-us/help.htm)
-#
-# To run the script, you must have installed Python 2.7.9 or later.
+# To run the script, you must have installed Python 2.7.X or 3.3 and later.
####
-import tableauserverclient as TSC
import argparse
import getpass
import logging
-parser = argparse.ArgumentParser(description='List workbooks on site, with option set to ignore SSL verification.')
-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)')
-args = parser.parse_args()
+import tableauserverclient as TSC
+
+
+def main():
+
+ parser = argparse.ArgumentParser(description='List workbooks on site, with option set to ignore SSL verification.')
+ 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)')
+
+ args = parser.parse_args()
+
+ 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)
-password = getpass.getpass("Password: ")
+ # Step 1: Create required objects for sign in
+ tableau_auth = TSC.TableauAuth(args.username, password)
+ server = TSC.Server(args.server)
-# 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 2: Set http options to disable verifying SSL
+ server.add_http_options({'verify': False})
-# Step 1: Create required objects for sign in
-tableau_auth = TSC.TableauAuth(args.username, password)
-server = TSC.Server(args.server)
+ with server.auth.sign_in(tableau_auth):
-# Step 2: Set http options to disable verifying SSL
-server.add_http_options({'verify': False})
+ # Step 3: Query all workbooks and list them
+ all_workbooks, pagination_item = server.workbooks.get()
+ print('{0} workbooks found. Showing {1}:'.format(pagination_item.total_available, pagination_item.page_size))
+ for workbook in all_workbooks:
+ print('\t{0} (ID: {1})'.format(workbook.name, workbook.id))
-with server.auth.sign_in(tableau_auth):
- # Step 3: Query all workbooks and list them
- all_workbooks, pagination_item = server.workbooks.get()
- print('{0} workbooks found. Showing {1}:'.format(pagination_item.total_available, pagination_item.page_size))
- for workbook in all_workbooks:
- print('\t{0} (ID: {1})'.format(workbook.name, workbook.id))
+if __name__ == '__main__':
+ main()
diff --git a/setup.py b/setup.py
index c4b654ebf..e4214aa70 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@
setup(
name='tableauserverclient',
- version='0.1',
+ version='0.2',
author='Tableau',
author_email='github@tableau.com',
url='https://github.com/tableau/server-client-python',
diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py
index 7ac613556..c7a628d83 100644
--- a/tableauserverclient/__init__.py
+++ b/tableauserverclient/__init__.py
@@ -1,9 +1,10 @@
from .namespace import NAMESPACE
-from .models import ConnectionItem, DatasourceItem,\
- GroupItem, PaginationItem, ProjectItem, \
- SiteItem, TableauAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError
+from .models import ConnectionCredentials, ConnectionItem, DatasourceItem,\
+ GroupItem, PaginationItem, ProjectItem, ScheduleItem, \
+ SiteItem, TableauAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \
+ HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem
from .server import RequestOptions, Filter, Sort, Server, ServerResponseError,\
- MissingRequiredFieldError, NotSignedInError
+ MissingRequiredFieldError, NotSignedInError, Pager
__version__ = '0.0.1'
__VERSION__ = __version__
diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py
index 594252f8f..b248ea399 100644
--- a/tableauserverclient/models/__init__.py
+++ b/tableauserverclient/models/__init__.py
@@ -1,9 +1,13 @@
+from .connection_credentials import ConnectionCredentials
from .connection_item import ConnectionItem
from .datasource_item import DatasourceItem
from .exceptions import UnpopulatedPropertyError
from .group_item import GroupItem
+from .interval_item import IntervalItem, DailyInterval, WeeklyInterval, MonthlyInterval, HourlyInterval
from .pagination_item import PaginationItem
from .project_item import ProjectItem
+from .schedule_item import ScheduleItem
+from .server_info_item import ServerInfoItem
from .site_item import SiteItem
from .tableau_auth import TableauAuth
from .user_item import UserItem
diff --git a/tableauserverclient/models/connection_credentials.py b/tableauserverclient/models/connection_credentials.py
new file mode 100644
index 000000000..d823b0b7f
--- /dev/null
+++ b/tableauserverclient/models/connection_credentials.py
@@ -0,0 +1,24 @@
+from .property_decorators import property_is_boolean
+
+
+class ConnectionCredentials(object):
+ """Connection Credentials for Workbooks and Datasources publish request.
+
+ Consider removing this object and other variables holding secrets
+ as soon as possible after use to avoid them hanging around in memory.
+
+ """
+
+ def __init__(self, name, password, embed=True):
+ self.name = name
+ self.password = password
+ self.embed = embed
+
+ @property
+ def embed(self):
+ return self._embed
+
+ @embed.setter
+ @property_is_boolean
+ def embed(self, value):
+ self._embed = value
diff --git a/tableauserverclient/models/datasource_item.py b/tableauserverclient/models/datasource_item.py
index 9f65f5419..3ae4c5743 100644
--- a/tableauserverclient/models/datasource_item.py
+++ b/tableauserverclient/models/datasource_item.py
@@ -1,5 +1,6 @@
import xml.etree.ElementTree as ET
from .exceptions import UnpopulatedPropertyError
+from .property_decorators import property_not_nullable
from .tag_item import TagItem
from .. import NAMESPACE
@@ -10,15 +11,12 @@ def __init__(self, project_id, name=None):
self._content_url = None
self._created_at = None
self._id = None
- self._project_id = None
self._project_name = None
self._tags = set()
self._datasource_type = None
self._updated_at = None
self.name = name
self.owner_id = None
-
- # Invoke setter
self.project_id = project_id
@property
@@ -45,12 +43,9 @@ def project_id(self):
return self._project_id
@project_id.setter
+ @property_not_nullable
def project_id(self, value):
- if value is None:
- error = 'Project ID must be defined.'
- raise ValueError(error)
- else:
- self._project_id = value
+ self._project_id = value
@property
def project_name(self):
diff --git a/tableauserverclient/models/group_item.py b/tableauserverclient/models/group_item.py
index 9e6de73cd..c0014eac0 100644
--- a/tableauserverclient/models/group_item.py
+++ b/tableauserverclient/models/group_item.py
@@ -1,5 +1,6 @@
import xml.etree.ElementTree as ET
from .exceptions import UnpopulatedPropertyError
+from .property_decorators import property_not_empty
from .. import NAMESPACE
@@ -7,10 +8,7 @@ class GroupItem(object):
def __init__(self, name):
self._domain_name = None
self._id = None
- self._name = None
self._users = None
-
- # Invoke setter
self.name = name
@property
@@ -26,12 +24,9 @@ def name(self):
return self._name
@name.setter
+ @property_not_empty
def name(self, value):
- if not value:
- error = 'Name must be defined.'
- raise ValueError(error)
- else:
- self._name = value
+ self._name = value
@property
def users(self):
diff --git a/tableauserverclient/models/interval_item.py b/tableauserverclient/models/interval_item.py
new file mode 100644
index 000000000..484ee709f
--- /dev/null
+++ b/tableauserverclient/models/interval_item.py
@@ -0,0 +1,183 @@
+from .property_decorators import property_is_valid_time, property_not_nullable
+
+
+class IntervalItem(object):
+ class Frequency:
+ Hourly = "Hourly"
+ Daily = "Daily"
+ Weekly = "Weekly"
+ Monthly = "Monthly"
+
+ class Occurrence:
+ Minutes = "minutes"
+ Hours = "hours"
+ WeekDay = "weekDay"
+ MonthDay = "monthDay"
+
+ class Day:
+ Sunday = "Sunday"
+ Monday = "Monday"
+ Tuesday = "Tuesday"
+ Wednesday = "Wednesday"
+ Thursday = "Thursday"
+ Friday = "Friday"
+ Saturday = "Saturday"
+ LastDay = "LastDay"
+
+
+class HourlyInterval(object):
+ def __init__(self, start_time, end_time, interval_value):
+
+ self.start_time = start_time
+ self.end_time = end_time
+ self.interval = interval_value
+
+ @property
+ def _frequency(self):
+ return IntervalItem.Frequency.Hourly
+
+ @property
+ def start_time(self):
+ return self._start_time
+
+ @start_time.setter
+ @property_is_valid_time
+ @property_not_nullable
+ def start_time(self, value):
+ self._start_time = value
+
+ @property
+ def end_time(self):
+ return self._end_time
+
+ @end_time.setter
+ @property_is_valid_time
+ @property_not_nullable
+ def end_time(self, value):
+ self._end_time = value
+
+ @property
+ def interval(self):
+ return self._interval
+
+ @interval.setter
+ def interval(self, interval):
+ VALID_INTERVALS = {.25, .5, 1, 2, 4, 6, 8, 12}
+ if float(interval) not in VALID_INTERVALS:
+ error = "Invalid interval {} not in {}".format(interval, str(VALID_INTERVALS))
+ raise ValueError(error)
+
+ self._interval = interval
+
+ def _interval_type_pairs(self):
+
+ # We use fractional hours for the two minute-based intervals.
+ # Need to convert to minutes from hours here
+ if self.interval in {.25, .5}:
+ calculated_interval = int(self.interval * 60)
+ interval_type = IntervalItem.Occurrence.Minutes
+ else:
+ calculated_interval = self.interval
+ interval_type = IntervalItem.Occurrence.Hours
+
+ return [(interval_type, str(calculated_interval))]
+
+
+class DailyInterval(object):
+ def __init__(self, start_time):
+ self.start_time = start_time
+
+ @property
+ def _frequency(self):
+ return IntervalItem.Frequency.Daily
+
+ @property
+ def start_time(self):
+ return self._start_time
+
+ @start_time.setter
+ @property_is_valid_time
+ @property_not_nullable
+ def start_time(self, value):
+ self._start_time = value
+
+
+class WeeklyInterval(object):
+ def __init__(self, start_time, *interval_values):
+ self.start_time = start_time
+ self.interval = interval_values
+
+ @property
+ def _frequency(self):
+ return IntervalItem.Frequency.Weekly
+
+ @property
+ def start_time(self):
+ return self._start_time
+
+ @start_time.setter
+ @property_is_valid_time
+ @property_not_nullable
+ def start_time(self, value):
+ self._start_time = value
+
+ @property
+ def interval(self):
+ return self._interval
+
+ @interval.setter
+ def interval(self, interval_values):
+ if not all(hasattr(IntervalItem.Day, day) for day in interval_values):
+ raise ValueError("Invalid week day defined " + str(interval_values))
+
+ self._interval = interval_values
+
+ def _interval_type_pairs(self):
+ return [(IntervalItem.Occurrence.WeekDay, day) for day in self.interval]
+
+
+class MonthlyInterval(object):
+ def __init__(self, start_time, interval_value):
+ self.start_time = start_time
+ self.interval = str(interval_value)
+
+ @property
+ def _frequency(self):
+ return IntervalItem.Frequency.Monthly
+
+ @property
+ def start_time(self):
+ return self._start_time
+
+ @start_time.setter
+ @property_is_valid_time
+ @property_not_nullable
+ def start_time(self, value):
+ self._start_time = value
+
+ @property
+ def interval(self):
+ return self._interval
+
+ @interval.setter
+ def interval(self, interval_value):
+ error = "Invalid interval value for a monthly frequency: {}.".format(interval_value)
+
+ # This is weird because the value could be a str or an int
+ # The only valid str is 'LastDay' so we check that first. If that's not it
+ # try to convert it to an int, if that fails because it's an incorrect string
+ # like 'badstring' we catch and re-raise. Otherwise we convert to int and check
+ # that it's in range 1-31
+
+ if interval_value != "LastDay":
+ try:
+ if not (1 <= int(interval_value) <= 31):
+ raise ValueError(error)
+ except ValueError as e:
+ if interval_value != "LastDay":
+ raise ValueError(error)
+
+ self._interval = str(interval_value)
+
+ def _interval_type_pairs(self):
+ return [(IntervalItem.Occurrence.MonthDay, self.interval)]
diff --git a/tableauserverclient/models/project_item.py b/tableauserverclient/models/project_item.py
index 768d5f16e..b60a62633 100644
--- a/tableauserverclient/models/project_item.py
+++ b/tableauserverclient/models/project_item.py
@@ -1,4 +1,5 @@
import xml.etree.ElementTree as ET
+from .property_decorators import property_is_enum, property_not_empty
from .. import NAMESPACE
@@ -10,28 +11,18 @@ class ContentPermissions:
def __init__(self, name, description=None, content_permissions=None):
self._content_permissions = None
self._id = None
- self._name = None
self.description = description
-
- # Invoke setter
self.name = name
-
- if content_permissions:
- # In order to invoke the setter method to validate content_permissions,
- # _content_permissions must be initialized first.
- self.content_permissions = content_permissions
+ self.content_permissions = content_permissions
@property
def content_permissions(self):
return self._content_permissions
@content_permissions.setter
+ @property_is_enum(ContentPermissions)
def content_permissions(self, value):
- if value and not hasattr(ProjectItem.ContentPermissions, value):
- error = 'Invalid content permission defined.'
- raise ValueError(error)
- else:
- self._content_permissions = value
+ self._content_permissions = value
@property
def id(self):
@@ -42,12 +33,9 @@ def name(self):
return self._name
@name.setter
+ @property_not_empty
def name(self, value):
- if not value:
- error = 'Name must be defined.'
- raise ValueError(error)
- else:
- self._name = value
+ self._name = value
def is_default(self):
return self.name.lower() == 'default'
diff --git a/tableauserverclient/models/property_decorators.py b/tableauserverclient/models/property_decorators.py
new file mode 100644
index 000000000..de8fe8d8c
--- /dev/null
+++ b/tableauserverclient/models/property_decorators.py
@@ -0,0 +1,101 @@
+import re
+from functools import wraps
+
+
+def property_is_enum(enum_type):
+ def property_type_decorator(func):
+ @wraps(func)
+ def wrapper(self, value):
+ if value is not None and not hasattr(enum_type, value):
+ error = "Invalid value: {0}. {1} must be of type {2}.".format(value, func.__name__, enum_type.__name__)
+ raise ValueError(error)
+ return func(self, value)
+
+ return wrapper
+
+ return property_type_decorator
+
+
+def property_is_boolean(func):
+ @wraps(func)
+ def wrapper(self, value):
+ if not isinstance(value, bool):
+ error = "Boolean expected for {0} flag.".format(func.__name__)
+ raise ValueError(error)
+ return func(self, value)
+
+ return wrapper
+
+
+def property_not_nullable(func):
+ @wraps(func)
+ def wrapper(self, value):
+ if value is None:
+ error = "{0} must be defined.".format(func.__name__)
+ raise ValueError(error)
+ return func(self, value)
+
+ return wrapper
+
+
+def property_not_empty(func):
+ @wraps(func)
+ def wrapper(self, value):
+ if not value:
+ error = "{0} must not be empty.".format(func.__name__)
+ raise ValueError(error)
+ return func(self, value)
+
+ return wrapper
+
+
+def property_is_valid_time(func):
+ @wraps(func)
+ def wrapper(self, value):
+ units_of_time = {"hour", "minute", "second"}
+
+ if not any(hasattr(value, unit) for unit in units_of_time):
+ error = "Invalid time object defined."
+ raise ValueError(error)
+ return func(self, value)
+
+ return wrapper
+
+
+def property_is_int(range):
+ def property_type_decorator(func):
+ @wraps(func)
+ def wrapper(self, value):
+ error = "Invalid priority defined: {}.".format(value)
+
+ if range is None:
+ if isinstance(value, int):
+ return func(self, value)
+ else:
+ raise ValueError(error)
+
+ min, max = range
+
+ if value < min or value > max:
+
+ raise ValueError(error)
+
+ return func(self, value)
+
+ return wrapper
+
+ return property_type_decorator
+
+
+def property_matches(regex_to_match, error):
+
+ compiled_re = re.compile(regex_to_match)
+
+ def wrapper(func):
+ @wraps(func)
+ def validate_regex_decorator(self, value):
+ if not compiled_re.match(value):
+ raise ValueError(error)
+ return func(self, value)
+ return validate_regex_decorator
+ return wrapper
diff --git a/tableauserverclient/models/schedule_item.py b/tableauserverclient/models/schedule_item.py
new file mode 100644
index 000000000..b0f7d1edb
--- /dev/null
+++ b/tableauserverclient/models/schedule_item.py
@@ -0,0 +1,229 @@
+import xml.etree.ElementTree as ET
+from datetime import datetime
+
+from .interval_item import IntervalItem, HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval
+from .property_decorators import property_is_enum, property_not_nullable, property_is_int
+from .. import NAMESPACE
+
+
+class ScheduleItem(object):
+ class Type:
+ Extract = "Extract"
+ Subscription = "Subscription"
+
+ class ExecutionOrder:
+ Parallel = "Parallel"
+ Serial = "Serial"
+
+ class State:
+ Active = "Active"
+ Suspended = "Suspended"
+
+ def __init__(self, name, priority, schedule_type, execution_order, interval_item):
+ self._created_at = None
+ self._end_schedule_at = None
+ self._id = None
+ self._next_run_at = None
+ self._state = None
+ self._updated_at = None
+ self.interval_item = interval_item
+ self.execution_order = execution_order
+ self.name = name
+ self.priority = priority
+ self.schedule_type = schedule_type
+
+ @property
+ def created_at(self):
+ return self._created_at
+
+ @property
+ def end_schedule_at(self):
+ return self._end_schedule_at
+
+ @property
+ def execution_order(self):
+ return self._execution_order
+
+ @execution_order.setter
+ @property_is_enum(ExecutionOrder)
+ def execution_order(self, value):
+ self._execution_order = value
+
+ @property
+ def id(self):
+ return self._id
+
+ @property
+ def name(self):
+ return self._name
+
+ @name.setter
+ @property_not_nullable
+ def name(self, value):
+ self._name = value
+
+ @property
+ def next_run_at(self):
+ return self._next_run_at
+
+ @property
+ def priority(self):
+ return self._priority
+
+ @priority.setter
+ @property_is_int(range=(1, 100))
+ def priority(self, value):
+ self._priority = value
+
+ @property
+ def schedule_type(self):
+ return self._schedule_type
+
+ @schedule_type.setter
+ @property_is_enum(Type)
+ @property_not_nullable
+ def schedule_type(self, value):
+ self._schedule_type = value
+
+ @property
+ def state(self):
+ return self._state
+
+ @state.setter
+ @property_is_enum(State)
+ def state(self, value):
+ self._state = value
+
+ @property
+ def updated_at(self):
+ return self._updated_at
+
+ def _parse_common_tags(self, schedule_xml):
+ if not isinstance(schedule_xml, ET.Element):
+ schedule_xml = ET.fromstring(schedule_xml).find('.//t:schedule', namespaces=NAMESPACE)
+ if schedule_xml is not None:
+ (_, name, _, _, updated_at, _, next_run_at, end_schedule_at, execution_order,
+ priority, interval_item) = self._parse_element(schedule_xml)
+
+ self._set_values(id=None,
+ name=name,
+ state=None,
+ created_at=None,
+ updated_at=updated_at,
+ schedule_type=None,
+ next_run_at=next_run_at,
+ end_schedule_at=end_schedule_at,
+ execution_order=execution_order,
+ priority=priority,
+ interval_item=interval_item)
+
+ return self
+
+ def _set_values(self, id, name, state, created_at, updated_at, schedule_type,
+ next_run_at, end_schedule_at, execution_order, priority, interval_item):
+ if id is not None:
+ self._id = id
+ if name:
+ self._name = name
+ if state:
+ self._state = state
+ if created_at:
+ self._created_at = created_at
+ if updated_at:
+ self._updated_at = updated_at
+ if schedule_type:
+ self._schedule_type = schedule_type
+ if next_run_at:
+ self._next_run_at = next_run_at
+ if end_schedule_at:
+ self._end_schedule_at = end_schedule_at
+ if execution_order:
+ self._execution_order = execution_order
+ if priority:
+ self._priority = priority
+ if interval_item:
+ self._interval_item = interval_item
+
+ @classmethod
+ def from_response(cls, resp):
+ all_schedule_items = []
+ parsed_response = ET.fromstring(resp)
+ all_schedule_xml = parsed_response.findall('.//t:schedule', namespaces=NAMESPACE)
+ for schedule_xml in all_schedule_xml:
+ (id, name, state, created_at, updated_at, schedule_type, next_run_at,
+ end_schedule_at, execution_order, priority, interval_item) = cls._parse_element(schedule_xml)
+
+ schedule_item = cls(name, priority, schedule_type, execution_order, interval_item)
+
+ schedule_item._set_values(id=id,
+ name=None,
+ state=state,
+ created_at=created_at,
+ updated_at=updated_at,
+ schedule_type=None,
+ next_run_at=next_run_at,
+ end_schedule_at=end_schedule_at,
+ execution_order=None,
+ priority=None,
+ interval_item=None)
+
+ all_schedule_items.append(schedule_item)
+ return all_schedule_items
+
+ @staticmethod
+ def _parse_interval_item(parsed_response, frequency):
+ start_time = parsed_response.get("start", None)
+ start_time = datetime.strptime(start_time, "%H:%M:%S").time()
+ end_time = parsed_response.get("end", None)
+ if end_time is not None:
+ end_time = datetime.strptime(end_time, "%H:%M:%S").time()
+ interval_elems = parsed_response.findall(".//t:intervals/t:interval", namespaces=NAMESPACE)
+ interval = []
+ for interval_elem in interval_elems:
+ interval.extend(interval_elem.attrib.items())
+
+ if frequency == IntervalItem.Frequency.Daily:
+ return DailyInterval(start_time)
+
+ if frequency == IntervalItem.Frequency.Hourly:
+ interval_occurrence, interval_value = interval.pop()
+
+ # We use fractional hours for the two minute-based intervals.
+ # Need to convert to hours from minutes here
+ if interval_occurrence == IntervalItem.Occurrence.Minutes:
+ interval_value = float(interval_value / 60)
+
+ return HourlyInterval(start_time, end_time, interval_value)
+
+ if frequency == IntervalItem.Frequency.Weekly:
+ interval_values = [i[1] for i in interval]
+ return WeeklyInterval(start_time, *interval_values)
+
+ if frequency == IntervalItem.Frequency.Monthly:
+ interval_occurrence, interval_value = interval.pop()
+ return MonthlyInterval(start_time, interval_value)
+
+ @staticmethod
+ def _parse_element(schedule_xml):
+ id = schedule_xml.get('id', None)
+ name = schedule_xml.get('name', None)
+ state = schedule_xml.get('state', None)
+ created_at = schedule_xml.get('createdAt', None)
+ updated_at = schedule_xml.get('updatedAt', None)
+ schedule_type = schedule_xml.get('type', None)
+ frequency = schedule_xml.get('frequency', None)
+ next_run_at = schedule_xml.get('nextRunAt', None)
+ end_schedule_at = schedule_xml.get('endScheduleAt', None)
+ execution_order = schedule_xml.get('executionOrder', None)
+
+ priority = schedule_xml.get('priority', None)
+ if priority:
+ priority = int(priority)
+
+ interval_item = None
+ frequency_detail_elem = schedule_xml.find('.//t:frequencyDetails', namespaces=NAMESPACE)
+ if frequency_detail_elem is not None:
+ interval_item = ScheduleItem._parse_interval_item(frequency_detail_elem, frequency)
+
+ return id, name, state, created_at, updated_at, schedule_type, \
+ next_run_at, end_schedule_at, execution_order, priority, interval_item
diff --git a/tableauserverclient/models/server_info_item.py b/tableauserverclient/models/server_info_item.py
new file mode 100644
index 000000000..91900f850
--- /dev/null
+++ b/tableauserverclient/models/server_info_item.py
@@ -0,0 +1,33 @@
+import xml.etree.ElementTree as ET
+from .. import NAMESPACE
+
+
+class ServerInfoItem(object):
+ def __init__(self, product_version, build_number, rest_api_version):
+ self._product_version = product_version
+ self._build_number = build_number
+ self._rest_api_version = rest_api_version
+
+ @property
+ def product_version(self):
+ return self._product_version
+
+ @property
+ def build_number(self):
+ return self._build_number
+
+ @property
+ def rest_api_version(self):
+ return self._rest_api_version
+
+ @classmethod
+ def from_response(cls, resp):
+ parsed_response = ET.fromstring(resp)
+ product_version_tag = parsed_response.find('.//t:productVersion', namespaces=NAMESPACE)
+ rest_api_version_tag = parsed_response.find('.//t:restApiVersion', namespaces=NAMESPACE)
+
+ build_number = product_version_tag.get('build', None)
+ product_version = product_version_tag.text
+ rest_api_version = rest_api_version_tag.text
+
+ return cls(product_version, build_number, rest_api_version)
diff --git a/tableauserverclient/models/site_item.py b/tableauserverclient/models/site_item.py
index f2791ed6a..40a49e453 100644
--- a/tableauserverclient/models/site_item.py
+++ b/tableauserverclient/models/site_item.py
@@ -1,7 +1,12 @@
import xml.etree.ElementTree as ET
+from .property_decorators import (property_is_enum, property_is_boolean, property_matches,
+ property_not_empty, property_not_nullable)
from .. import NAMESPACE
+VALID_CONTENT_URL_RE = r"^[a-zA-Z0-9_\-]*$"
+
+
class SiteItem(object):
class AdminMode:
ContentAndUsers = 'ContentAndUsers'
@@ -14,67 +19,48 @@ class State:
def __init__(self, name, content_url, admin_mode=None, user_quota=None, storage_quota=None,
disable_subscriptions=False, subscribe_others_enabled=True, revision_history_enabled=False):
self._admin_mode = None
- self._content_url = None
- self._disable_subscriptions = None
self._id = None
- self._name = None
self._num_users = None
- self._revision_history_enabled = None
self._state = None
self._status_reason = None
self._storage = None
- self._subscribe_others_enabled = None
self.revision_limit = None
self.user_quota = user_quota
self.storage_quota = storage_quota
-
- # Invoke setter
self.content_url = content_url
self.disable_subscriptions = disable_subscriptions
self.name = name
self.revision_history_enabled = revision_history_enabled
self.subscribe_others_enabled = subscribe_others_enabled
-
- if admin_mode:
- # In order to invoke the setter method to validate admin_mode,
- # _admin_mode must be initialized first.
- self.admin_mode = admin_mode
+ self.admin_mode = admin_mode
@property
def admin_mode(self):
return self._admin_mode
@admin_mode.setter
+ @property_is_enum(AdminMode)
def admin_mode(self, value):
- if value and not hasattr(SiteItem.AdminMode, value):
- error = 'Invalid admin mode defined.'
- raise ValueError(error)
- else:
- self._admin_mode = value
+ self._admin_mode = value
@property
def content_url(self):
return self._content_url
@content_url.setter
+ @property_not_nullable
+ @property_matches(VALID_CONTENT_URL_RE, "content_url can contain only letters, numbers, dashes, and underscores")
def content_url(self, value):
- if value is None:
- error = 'Content URL must be defined.'
- raise ValueError(error)
- else:
- self._content_url = value
+ self._content_url = value
@property
def disable_subscriptions(self):
return self._disable_subscriptions
@disable_subscriptions.setter
+ @property_is_boolean
def disable_subscriptions(self, value):
- if not isinstance(value, bool):
- error = 'Boolean expected for disable_subscriptions flag.'
- raise ValueError(error)
- else:
- self._disable_subscriptions = value
+ self._disable_subscriptions = value
@property
def id(self):
@@ -85,12 +71,9 @@ def name(self):
return self._name
@name.setter
+ @property_not_empty
def name(self, value):
- if not value:
- error = 'Name must be defined.'
- raise ValueError(error)
- else:
- self._name = value
+ self._name = value
@property
def num_users(self):
@@ -101,24 +84,18 @@ def revision_history_enabled(self):
return self._revision_history_enabled
@revision_history_enabled.setter
+ @property_is_boolean
def revision_history_enabled(self, value):
- if not isinstance(value, bool):
- error = 'Boolean expected for revision_history_enabled flag.'
- raise ValueError(error)
- else:
- self._revision_history_enabled = value
+ self._revision_history_enabled = value
@property
def state(self):
return self._state
@state.setter
+ @property_is_enum(State)
def state(self, value):
- if not hasattr(SiteItem.State, value):
- error = 'Invalid state defined.'
- raise ValueError(error)
- else:
- self._state = value
+ self._state = value
@property
def status_reason(self):
@@ -133,12 +110,9 @@ def subscribe_others_enabled(self):
return self._subscribe_others_enabled
@subscribe_others_enabled.setter
+ @property_is_boolean
def subscribe_others_enabled(self, value):
- if not isinstance(value, bool):
- error = 'Boolean expected for subscribe_others_enabled flag.'
- raise ValueError(error)
- else:
- self._subscribe_others_enabled = value
+ self._subscribe_others_enabled = value
def is_default(self):
return self.name.lower() == 'default'
diff --git a/tableauserverclient/models/tableau_auth.py b/tableauserverclient/models/tableau_auth.py
index cdf0fb410..7670e2812 100644
--- a/tableauserverclient/models/tableau_auth.py
+++ b/tableauserverclient/models/tableau_auth.py
@@ -1,6 +1,20 @@
class TableauAuth(object):
- def __init__(self, username, password, site='', user_id_to_impersonate=None):
+ def __init__(self, username, password, site=None, site_id='', user_id_to_impersonate=None):
+ if site is not None:
+ import warnings
+ warnings.warn('TableauAuth(...site=""...) is deprecated, '
+ 'please use TableauAuth(...site_id=""...) instead.',
+ DeprecationWarning)
+ site_id = site
+
self.user_id_to_impersonate = user_id_to_impersonate
self.password = password
- self.site = site
+ self.site_id = site_id
self.username = username
+
+ @property
+ def site(self):
+ import warnings
+ warnings.warn('TableauAuth.site is deprecated, use TableauAuth.site_id instead.',
+ DeprecationWarning)
+ return self.site_id
diff --git a/tableauserverclient/models/user_item.py b/tableauserverclient/models/user_item.py
index a63db6908..49a048f69 100644
--- a/tableauserverclient/models/user_item.py
+++ b/tableauserverclient/models/user_item.py
@@ -1,5 +1,6 @@
import xml.etree.ElementTree as ET
from .exceptions import UnpopulatedPropertyError
+from .property_decorators import property_is_enum, property_not_empty, property_not_nullable
from .. import NAMESPACE
@@ -13,6 +14,7 @@ class Roles:
UnlicensedWithPublish = 'UnlicensedWithPublish'
Viewer = 'Viewer'
ViewerWithPublish = 'ViewerWithPublish'
+ Guest = 'Guest'
class Auth:
SAML = 'SAML'
@@ -24,33 +26,21 @@ def __init__(self, name, site_role, auth_setting=None):
self._external_auth_user_id = None
self._id = None
self._last_login = None
- self._name = None
- self._site_role = None
self._workbooks = None
self.email = None
self.fullname = None
- self.password = None
-
- # Invoke setter
self.name = name
self.site_role = site_role
-
- if auth_setting:
- # In order to invoke the setter method for auth_setting,
- # _auth_setting must be initialized first
- self.auth_setting = auth_setting
+ self.auth_setting = auth_setting
@property
def auth_setting(self):
return self._auth_setting
@auth_setting.setter
+ @property_is_enum(Auth)
def auth_setting(self, value):
- if not hasattr(UserItem.Auth, value):
- error = 'Invalid auth setting defined.'
- raise ValueError(error)
- else:
- self._auth_setting = value
+ self._auth_setting = value
@property
def domain_name(self):
@@ -73,27 +63,19 @@ def name(self):
return self._name
@name.setter
+ @property_not_empty
def name(self, value):
- if not value:
- error = 'Name must be defined.'
- raise ValueError(error)
- else:
- self._name = value
+ self._name = value
@property
def site_role(self):
return self._site_role
@site_role.setter
+ @property_not_nullable
+ @property_is_enum(Roles)
def site_role(self, value):
- if not value:
- error = 'Site role must be defined.'
- raise ValueError(error)
- elif not hasattr(UserItem.Roles, value):
- error = 'Invalid site role defined.'
- raise ValueError(error)
- else:
- self._site_role = value
+ self._site_role = value
@property
def workbooks(self):
diff --git a/tableauserverclient/models/workbook_item.py b/tableauserverclient/models/workbook_item.py
index 01d1b5109..9ccde5606 100644
--- a/tableauserverclient/models/workbook_item.py
+++ b/tableauserverclient/models/workbook_item.py
@@ -1,5 +1,6 @@
import xml.etree.ElementTree as ET
from .exceptions import UnpopulatedPropertyError
+from .property_decorators import property_not_nullable, property_is_boolean
from .tag_item import TagItem
from .view_item import ViewItem
from .. import NAMESPACE
@@ -14,17 +15,13 @@ def __init__(self, project_id, name=None, show_tabs=False):
self._id = None
self._initial_tags = set()
self._preview_image = None
- self._project_id = None
self._project_name = None
- self._show_tabs = None
self._size = None
self._updated_at = None
self._views = None
self.name = name
self.owner_id = None
self.tags = set()
-
- # Invoke setter
self.project_id = project_id
self.show_tabs = show_tabs
@@ -59,12 +56,9 @@ def project_id(self):
return self._project_id
@project_id.setter
+ @property_not_nullable
def project_id(self, value):
- if value is None:
- error = 'Project ID must be defined.'
- raise ValueError(error)
- else:
- self._project_id = value
+ self._project_id = value
@property
def project_name(self):
@@ -75,12 +69,9 @@ def show_tabs(self):
return self._show_tabs
@show_tabs.setter
+ @property_is_boolean
def show_tabs(self, value):
- if not isinstance(value, bool):
- error = 'Boolean expected for show tabs flag.'
- raise ValueError(error)
- else:
- self._show_tabs = value
+ self._show_tabs = value
@property
def size(self):
diff --git a/tableauserverclient/server/__init__.py b/tableauserverclient/server/__init__.py
index 909705052..e74e3cea6 100644
--- a/tableauserverclient/server/__init__.py
+++ b/tableauserverclient/server/__init__.py
@@ -3,9 +3,10 @@
from .filter import Filter
from .sort import Sort
from .. import ConnectionItem, DatasourceItem,\
- GroupItem, PaginationItem, ProjectItem, SiteItem, TableauAuth,\
+ GroupItem, PaginationItem, ProjectItem, ScheduleItem, SiteItem, TableauAuth,\
UserItem, ViewItem, WorkbookItem, NAMESPACE
-from .endpoint import Auth, Datasources, Endpoint, \
- Groups, Projects, Sites, Users, Views, Workbooks, ServerResponseError, MissingRequiredFieldError
+from .endpoint import Auth, Datasources, Endpoint, Groups, Projects, Schedules, \
+ Sites, Users, Views, Workbooks, ServerResponseError, MissingRequiredFieldError
from .server import Server
+from .pager import Pager
from .exceptions import NotSignedInError
diff --git a/tableauserverclient/server/endpoint/__init__.py b/tableauserverclient/server/endpoint/__init__.py
index 30adf2549..63d69510c 100644
--- a/tableauserverclient/server/endpoint/__init__.py
+++ b/tableauserverclient/server/endpoint/__init__.py
@@ -4,6 +4,8 @@
from .exceptions import ServerResponseError, MissingRequiredFieldError
from .groups_endpoint import Groups
from .projects_endpoint import Projects
+from .schedules_endpoint import Schedules
+from .server_info_endpoint import ServerInfo
from .sites_endpoint import Sites
from .users_endpoint import Users
from .views_endpoint import Views
diff --git a/tableauserverclient/server/endpoint/auth_endpoint.py b/tableauserverclient/server/endpoint/auth_endpoint.py
index e685effbe..ed42d32e6 100644
--- a/tableauserverclient/server/endpoint/auth_endpoint.py
+++ b/tableauserverclient/server/endpoint/auth_endpoint.py
@@ -17,10 +17,6 @@ def __enter__(self):
def __exit__(self, exc_type, exc_val, exc_tb):
self._callback()
- def __init__(self, parent_srv):
- super(Endpoint, self).__init__()
- self.parent_srv = parent_srv
-
@property
def baseurl(self):
return "{0}/auth".format(self.parent_srv.baseurl)
@@ -41,6 +37,9 @@ def sign_in(self, auth_req):
def sign_out(self):
url = "{0}/{1}".format(self.baseurl, 'signout')
+ # If there are no auth tokens you're already signed out. No-op
+ if not self.parent_srv.is_signed_in():
+ return
self.post_request(url, '')
self.parent_srv._clear_auth()
logger.info('Signed out')
diff --git a/tableauserverclient/server/endpoint/datasources_endpoint.py b/tableauserverclient/server/endpoint/datasources_endpoint.py
index ba49c768f..e8e4e4bf6 100644
--- a/tableauserverclient/server/endpoint/datasources_endpoint.py
+++ b/tableauserverclient/server/endpoint/datasources_endpoint.py
@@ -16,10 +16,6 @@
class Datasources(Endpoint):
- def __init__(self, parent_srv):
- super(Endpoint, self).__init__()
- self.parent_srv = parent_srv
-
@property
def baseurl(self):
return "{0}/sites/{1}/datasources".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@@ -94,7 +90,7 @@ def update(self, datasource_item):
return updated_datasource._parse_common_tags(server_response.content)
# Publish datasource
- def publish(self, datasource_item, file_path, mode):
+ def publish(self, datasource_item, file_path, mode, connection_credentials=None):
if not os.path.isfile(file_path):
error = "File path does not lead to an existing file."
raise IOError(error)
@@ -122,14 +118,16 @@ def publish(self, datasource_item, file_path, mode):
logger.info('Publishing {0} to server with chunking method (datasource over 64MB)'.format(filename))
upload_session_id = Fileuploads.upload_chunks(self.parent_srv, file_path)
url = "{0}&uploadSessionId={1}".format(url, upload_session_id)
- xml_request, content_type = RequestFactory.Datasource.publish_req_chunked(datasource_item)
+ xml_request, content_type = RequestFactory.Datasource.publish_req_chunked(datasource_item,
+ connection_credentials)
else:
logger.info('Publishing {0} to server'.format(filename))
with open(file_path, 'rb') as f:
file_contents = f.read()
xml_request, content_type = RequestFactory.Datasource.publish_req(datasource_item,
filename,
- file_contents)
+ file_contents,
+ connection_credentials)
server_response = self.post_request(url, xml_request, content_type)
new_datasource = DatasourceItem.from_response(server_response.content)[0]
logger.info('Published {0} (ID: {1})'.format(filename, new_datasource.id))
diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py
index 98f451211..c90b91004 100644
--- a/tableauserverclient/server/endpoint/endpoint.py
+++ b/tableauserverclient/server/endpoint/endpoint.py
@@ -1,60 +1,70 @@
from .exceptions import ServerResponseError
import logging
+
logger = logging.getLogger('tableau.endpoint')
Success_codes = [200, 201, 204]
class Endpoint(object):
- def __init__(self):
- self.parent_srv = None
+ def __init__(self, parent_srv):
+ self.parent_srv = parent_srv
@staticmethod
- def _check_status(server_response):
- if server_response.status_code not in Success_codes:
- raise ServerResponseError.from_response(server_response.content)
+ def _make_common_headers(auth_token, content_type):
+ headers = {}
+ if auth_token is not None:
+ headers['x-tableau-auth'] = auth_token
+ if content_type is not None:
+ headers['content-type'] = content_type
- def get_request(self, url, request_object=None):
+ return headers
+
+ def _make_request(self, method, url, content=None, request_object=None, auth_token=None, content_type=None):
if request_object is not None:
url = request_object.apply_query_params(url)
- auth_token = self.parent_srv.auth_token
- server_response = self.parent_srv.session.get(url,
- headers={'x-tableau-auth': auth_token},
- **self.parent_srv.http_options)
+ parameters = {}
+ parameters.update(self.parent_srv.http_options)
+ parameters['headers'] = Endpoint._make_common_headers(auth_token, content_type)
+
+ if content is not None:
+ parameters['data'] = content
+
+ server_response = method(url, **parameters)
self._check_status(server_response)
+
+ # This check is to determine if the response is a text response (xml or otherwise)
+ # so that we do not attempt to log bytes and other binary data.
if server_response.encoding:
- logger.debug(u'Server response from {0}: \n\t{1}'.format(
+ logger.debug(u'Server response from {0}:\n\t{1}'.format(
url, server_response.content.decode(server_response.encoding)))
return server_response
+ @staticmethod
+ def _check_status(server_response):
+ if server_response.status_code not in Success_codes:
+ raise ServerResponseError.from_response(server_response.content)
+
+ def get_unauthenticated_request(self, url, request_object=None):
+ return self._make_request(self.parent_srv.session.get, url, request_object=request_object)
+
+ def get_request(self, url, request_object=None):
+ return self._make_request(self.parent_srv.session.get, url, auth_token=self.parent_srv.auth_token,
+ request_object=request_object)
+
def delete_request(self, url):
- auth_token = self.parent_srv.auth_token
- server_response = self.parent_srv.session.delete(url,
- headers={'x-tableau-auth': auth_token},
- **self.parent_srv.http_options)
- self._check_status(server_response)
+ # We don't return anything for a delete
+ self._make_request(self.parent_srv.session.delete, url, auth_token=self.parent_srv.auth_token)
def put_request(self, url, xml_request, content_type='text/xml'):
- auth_token = self.parent_srv.auth_token
- server_response = self.parent_srv.session.put(url, data=xml_request,
- headers={'x-tableau-auth': auth_token,
- 'content-type': content_type},
- **self.parent_srv.http_options)
- self._check_status(server_response)
- if server_response.encoding:
- logger.debug(u'Server response from {0}: \n\t{1}'.format(
- url, server_response.content.decode(server_response.encoding)))
- return server_response
+ return self._make_request(self.parent_srv.session.put, url,
+ content=xml_request,
+ auth_token=self.parent_srv.auth_token,
+ content_type=content_type)
def post_request(self, url, xml_request, content_type='text/xml'):
- auth_token = self.parent_srv.auth_token
- server_response = self.parent_srv.session.post(url, data=xml_request,
- headers={'x-tableau-auth': auth_token,
- 'content-type': content_type},
- **self.parent_srv.http_options)
- self._check_status(server_response)
- if server_response.encoding:
- logger.debug(u'Server response from {0}: \n\t{1}'.format(
- url, server_response.content.decode(server_response.encoding)))
- return server_response
+ return self._make_request(self.parent_srv.session.post, url,
+ content=xml_request,
+ auth_token=self.parent_srv.auth_token,
+ content_type=content_type)
diff --git a/tableauserverclient/server/endpoint/exceptions.py b/tableauserverclient/server/endpoint/exceptions.py
index 4efac7bf5..7907a6dab 100644
--- a/tableauserverclient/server/endpoint/exceptions.py
+++ b/tableauserverclient/server/endpoint/exceptions.py
@@ -16,11 +16,9 @@ def __str__(self):
def from_response(cls, resp):
# Check elements exist before .text
parsed_response = ET.fromstring(resp)
- error_response = cls(
- parsed_response.find('t:error', namespaces=NAMESPACE).get('code', ''),
- parsed_response.find('.//t:summary', namespaces=NAMESPACE).text,
- parsed_response.find('.//t:detail', namespaces=NAMESPACE).text
- )
+ error_response = cls(parsed_response.find('t:error', namespaces=NAMESPACE).get('code', ''),
+ parsed_response.find('.//t:summary', namespaces=NAMESPACE).text,
+ parsed_response.find('.//t:detail', namespaces=NAMESPACE).text)
return error_response
diff --git a/tableauserverclient/server/endpoint/fileuploads_endpoint.py b/tableauserverclient/server/endpoint/fileuploads_endpoint.py
index 65a3b2526..df26d3db5 100644
--- a/tableauserverclient/server/endpoint/fileuploads_endpoint.py
+++ b/tableauserverclient/server/endpoint/fileuploads_endpoint.py
@@ -13,8 +13,7 @@
class Fileuploads(Endpoint):
def __init__(self, parent_srv):
- super(Endpoint, self).__init__()
- self.parent_srv = parent_srv
+ super(Fileuploads, self).__init__(parent_srv)
self.upload_id = ''
@property
diff --git a/tableauserverclient/server/endpoint/groups_endpoint.py b/tableauserverclient/server/endpoint/groups_endpoint.py
index d982ff13a..e1eb2ecfc 100644
--- a/tableauserverclient/server/endpoint/groups_endpoint.py
+++ b/tableauserverclient/server/endpoint/groups_endpoint.py
@@ -1,5 +1,6 @@
from .endpoint import Endpoint
from .exceptions import MissingRequiredFieldError
+from ...models.exceptions import UnpopulatedPropertyError
from .. import RequestFactory, GroupItem, UserItem, PaginationItem
import logging
@@ -7,10 +8,6 @@
class Groups(Endpoint):
- def __init__(self, parent_srv):
- super(Endpoint, self).__init__()
- self.parent_srv = parent_srv
-
@property
def baseurl(self):
return "{0}/sites/{1}/groups".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@@ -45,9 +42,39 @@ def delete(self, group_id):
self.delete_request(url)
logger.info('Deleted single group (ID: {0})'.format(group_id))
+ def create(self, group_item):
+ url = self.baseurl
+ create_req = RequestFactory.Group.create_req(group_item)
+ server_response = self.post_request(url, create_req)
+ return GroupItem.from_response(server_response.content)[0]
+
# Removes 1 user from 1 group
def remove_user(self, group_item, user_id):
- user_set = group_item.users
+ self._remove_user(group_item, user_id)
+ try:
+ user_set = group_item.users
+ for user in user_set:
+ if user.id == user_id:
+ user_set.remove(user)
+ break
+ except UnpopulatedPropertyError:
+ # If we aren't populated, do nothing to the user list
+ pass
+ logger.info('Removed user (id: {0}) from group (ID: {1})'.format(user_id, group_item.id))
+
+ # Adds 1 user to 1 group
+ def add_user(self, group_item, user_id):
+ new_user = self._add_user(group_item, user_id)
+ try:
+ user_set = group_item.users
+ user_set.add(new_user)
+ group_item._set_users(user_set)
+ except UnpopulatedPropertyError:
+ # If we aren't populated, do nothing to the user list
+ pass
+ logger.info('Added user (id: {0}) to group (ID: {1})'.format(user_id, group_item.id))
+
+ def _remove_user(self, group_item, user_id):
if not group_item.id:
error = "Group item missing ID."
raise MissingRequiredFieldError(error)
@@ -56,15 +83,8 @@ def remove_user(self, group_item, user_id):
raise ValueError(error)
url = "{0}/{1}/users/{2}".format(self.baseurl, group_item.id, user_id)
self.delete_request(url)
- for user in user_set:
- if user.id == user_id:
- user_set.remove(user)
- break
- logger.info('Removed user (id: {0}) from group (ID: {1})'.format(user_id, group_item.id))
- # Adds 1 user to 1 group
- def add_user(self, group_item, user_id):
- user_set = group_item.users
+ def _add_user(self, group_item, user_id):
if not group_item.id:
error = "Group item missing ID."
raise MissingRequiredFieldError(error)
@@ -74,7 +94,4 @@ def add_user(self, group_item, user_id):
url = "{0}/{1}/users".format(self.baseurl, group_item.id)
add_req = RequestFactory.Group.add_user_req(user_id)
server_response = self.post_request(url, add_req)
- new_user = UserItem.from_response(server_response.content).pop()
- user_set.add(new_user)
- group_item._set_users(user_set)
- logger.info('Added user (id: {0}) to group (ID: {1})'.format(user_id, group_item.id))
+ return UserItem.from_response(server_response.content).pop()
diff --git a/tableauserverclient/server/endpoint/projects_endpoint.py b/tableauserverclient/server/endpoint/projects_endpoint.py
index 484ee2aff..b146d4418 100644
--- a/tableauserverclient/server/endpoint/projects_endpoint.py
+++ b/tableauserverclient/server/endpoint/projects_endpoint.py
@@ -8,10 +8,6 @@
class Projects(Endpoint):
- def __init__(self, parent_srv):
- super(Endpoint, self).__init__()
- self.parent_srv = parent_srv
-
@property
def baseurl(self):
return "{0}/sites/{1}/projects".format(self.parent_srv.baseurl, self.parent_srv.site_id)
diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py
new file mode 100644
index 000000000..705f9577b
--- /dev/null
+++ b/tableauserverclient/server/endpoint/schedules_endpoint.py
@@ -0,0 +1,56 @@
+from .endpoint import Endpoint
+from .exceptions import MissingRequiredFieldError
+from .. import RequestFactory, PaginationItem, ScheduleItem
+import logging
+import copy
+
+logger = logging.getLogger('tableau.endpoint.schedules')
+
+
+class Schedules(Endpoint):
+ @property
+ def baseurl(self):
+ return "{0}/schedules".format(self.parent_srv.baseurl)
+
+ def get(self, req_options=None):
+ logger.info("Querying all schedules")
+ url = self.baseurl
+ server_response = self.get_request(url, req_options)
+ pagination_item = PaginationItem.from_response(server_response.content)
+ all_schedule_items = ScheduleItem.from_response(server_response.content)
+ return all_schedule_items, pagination_item
+
+ def delete(self, schedule_id):
+ if not schedule_id:
+ error = "Schedule ID undefined"
+ raise ValueError(error)
+ url = "{0}/{1}".format(self.baseurl, schedule_id)
+ self.delete_request(url)
+ logger.info("Deleted single schedule (ID: {0})".format(schedule_id))
+
+ def update(self, schedule_item):
+ if not schedule_item.id:
+ error = "Schedule item missing ID."
+ raise MissingRequiredFieldError(error)
+ if schedule_item.interval_item is None:
+ error = "Interval item must be defined."
+ raise MissingRequiredFieldError(error)
+
+ url = "{0}/{1}".format(self.baseurl, schedule_item.id)
+ update_req = RequestFactory.Schedule.update_req(schedule_item)
+ server_response = self.put_request(url, update_req)
+ logger.info("Updated schedule item (ID: {})".format(schedule_item.id))
+ updated_schedule = copy.copy(schedule_item)
+ return updated_schedule._parse_common_tags(server_response.content)
+
+ def create(self, schedule_item):
+ if schedule_item.interval_item is None:
+ error = "Interval item must be defined."
+ raise MissingRequiredFieldError(error)
+
+ url = self.baseurl
+ create_req = RequestFactory.Schedule.create_req(schedule_item)
+ server_response = self.post_request(url, create_req)
+ new_schedule = ScheduleItem.from_response(server_response.content)[0]
+ logger.info("Created new schedule (ID: {})".format(new_schedule.id))
+ return new_schedule
diff --git a/tableauserverclient/server/endpoint/server_info_endpoint.py b/tableauserverclient/server/endpoint/server_info_endpoint.py
new file mode 100644
index 000000000..1fb17f26f
--- /dev/null
+++ b/tableauserverclient/server/endpoint/server_info_endpoint.py
@@ -0,0 +1,17 @@
+from .endpoint import Endpoint
+from ...models import ServerInfoItem
+import logging
+
+logger = logging.getLogger('tableau.endpoint.server_info')
+
+
+class ServerInfo(Endpoint):
+ @property
+ def baseurl(self):
+ return "{0}/serverInfo".format(self.parent_srv.baseurl)
+
+ def get(self):
+ """ Retrieve the server info for the server. This is an unauthenticated call """
+ server_response = self.get_unauthenticated_request(self.baseurl)
+ server_info = ServerInfoItem.from_response(server_response.content)
+ return server_info
diff --git a/tableauserverclient/server/endpoint/sites_endpoint.py b/tableauserverclient/server/endpoint/sites_endpoint.py
index 704fb9de9..3977ad0f2 100644
--- a/tableauserverclient/server/endpoint/sites_endpoint.py
+++ b/tableauserverclient/server/endpoint/sites_endpoint.py
@@ -8,10 +8,6 @@
class Sites(Endpoint):
- def __init__(self, parent_srv):
- super(Endpoint, self).__init__()
- self.parent_srv = parent_srv
-
@property
def baseurl(self):
return "{0}/sites".format(self.parent_srv.baseurl)
@@ -59,7 +55,12 @@ def delete(self, site_id):
raise ValueError(error)
url = "{0}/{1}".format(self.baseurl, site_id)
self.delete_request(url)
- logger.info('Deleted single site (ID: {0})'.format(site_id))
+ # If we deleted the site we are logged into
+ # then we are automatically logged out
+ if site_id == self.parent_srv.site_id:
+ logger.info('Deleting current site and clearing auth tokens')
+ self.parent_srv._clear_auth()
+ logger.info('Deleted single site (ID: {0}) and signed out'.format(site_id))
# Create new site
def create(self, site_item):
diff --git a/tableauserverclient/server/endpoint/users_endpoint.py b/tableauserverclient/server/endpoint/users_endpoint.py
index ba5238d19..d5b5155fa 100644
--- a/tableauserverclient/server/endpoint/users_endpoint.py
+++ b/tableauserverclient/server/endpoint/users_endpoint.py
@@ -8,10 +8,6 @@
class Users(Endpoint):
- def __init__(self, parent_srv):
- super(Endpoint, self).__init__()
- self.parent_srv = parent_srv
-
@property
def baseurl(self):
return "{0}/sites/{1}/users".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@@ -36,13 +32,13 @@ def get_by_id(self, user_id):
return UserItem.from_response(server_response.content).pop()
# Update user
- def update(self, user_item):
+ def update(self, user_item, password=None):
if not user_item.id:
error = "User item missing ID."
raise MissingRequiredFieldError(error)
url = "{0}/{1}".format(self.baseurl, user_item.id)
- update_req = RequestFactory.User.update_req(user_item)
+ update_req = RequestFactory.User.update_req(user_item, password)
server_response = self.put_request(url, update_req)
logger.info('Updated user item (ID: {0})'.format(user_item.id))
updated_item = copy.copy(user_item)
diff --git a/tableauserverclient/server/endpoint/views_endpoint.py b/tableauserverclient/server/endpoint/views_endpoint.py
index 1ccc418f4..2b5f0e5dd 100644
--- a/tableauserverclient/server/endpoint/views_endpoint.py
+++ b/tableauserverclient/server/endpoint/views_endpoint.py
@@ -7,10 +7,6 @@
class Views(Endpoint):
- def __init__(self, parent_srv):
- super(Endpoint, self).__init__()
- self.parent_srv = parent_srv
-
@property
def baseurl(self):
return "{0}/sites/{1}".format(self.parent_srv.baseurl, self.parent_srv.site_id)
diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py
index e60789f06..6aabc6029 100644
--- a/tableauserverclient/server/endpoint/workbooks_endpoint.py
+++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py
@@ -17,10 +17,6 @@
class Workbooks(Endpoint):
- def __init__(self, parent_srv):
- super(Endpoint, self).__init__()
- self.parent_srv = parent_srv
-
@property
def baseurl(self):
return "{0}/sites/{1}/workbooks".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@@ -140,7 +136,7 @@ def populate_preview_image(self, workbook_item):
logger.info('Populated preview image for workbook (ID: {0})'.format(workbook_item.id))
# Publishes workbook. Chunking method if file over 64MB
- def publish(self, workbook_item, file_path, mode):
+ def publish(self, workbook_item, file_path, mode, connection_credentials=None):
if not os.path.isfile(file_path):
error = "File path does not lead to an existing file."
raise IOError(error)
@@ -171,14 +167,16 @@ def publish(self, workbook_item, file_path, mode):
logger.info('Publishing {0} to server with chunking method (workbook over 64MB)'.format(filename))
upload_session_id = Fileuploads.upload_chunks(self.parent_srv, file_path)
url = "{0}&uploadSessionId={1}".format(url, upload_session_id)
- xml_request, content_type = RequestFactory.Workbook.publish_req_chunked(workbook_item)
+ xml_request, content_type = RequestFactory.Workbook.publish_req_chunked(workbook_item,
+ connection_credentials)
else:
logger.info('Publishing {0} to server'.format(filename))
with open(file_path, 'rb') as f:
file_contents = f.read()
xml_request, content_type = RequestFactory.Workbook.publish_req(workbook_item,
filename,
- file_contents)
+ file_contents,
+ connection_credentials)
server_response = self.post_request(url, xml_request, content_type)
new_workbook = WorkbookItem.from_response(server_response.content)[0]
logger.info('Published {0} (ID: {1})'.format(filename, new_workbook.id))
diff --git a/tableauserverclient/server/pager.py b/tableauserverclient/server/pager.py
new file mode 100644
index 000000000..eaad398af
--- /dev/null
+++ b/tableauserverclient/server/pager.py
@@ -0,0 +1,43 @@
+from . import RequestOptions
+
+
+class Pager(object):
+ """
+ Generator that takes an endpoint with `.get` and lazily loads items from Server.
+ Supports all `RequestOptions` including starting on any page.
+ """
+
+ def __init__(self, endpoint, request_opts=None):
+ self._endpoint = endpoint.get
+ self._options = request_opts
+
+ # If we have options we could be starting on any page, backfill the count
+ if self._options:
+ self._count = ((self._options.pagenumber - 1) * self._options.pagesize)
+ else:
+ self._count = 0
+
+ def __iter__(self):
+ # Fetch the first page
+ current_item_list, last_pagination_item = self._endpoint(self._options)
+
+ # Get the rest on demand as a generator
+ while self._count < last_pagination_item.total_available:
+ if len(current_item_list) == 0:
+ current_item_list, last_pagination_item = self._load_next_page(last_pagination_item)
+
+ try:
+ yield current_item_list.pop(0)
+ self._count += 1
+
+ except IndexError:
+ # The total count on Server changed while fetching exit gracefully
+ raise StopIteration
+
+ def _load_next_page(self, last_pagination_item):
+ next_page = last_pagination_item.page_number + 1
+ opts = RequestOptions(pagenumber=next_page, pagesize=last_pagination_item.page_size)
+ if self._options is not None:
+ opts.sort, opts.filter = self._options.sort, self._options.filter
+ current_item_list, last_pagination_item = self._endpoint(opts)
+ return current_item_list, last_pagination_item
diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py
index 3439cfa43..9a9bf53e1 100644
--- a/tableauserverclient/server/request_factory.py
+++ b/tableauserverclient/server/request_factory.py
@@ -22,7 +22,7 @@ def signin_req(self, auth_item):
credentials_element.attrib['name'] = auth_item.username
credentials_element.attrib['password'] = auth_item.password
site_element = ET.SubElement(credentials_element, 'site')
- site_element.attrib['contentUrl'] = auth_item.site
+ site_element.attrib['contentUrl'] = auth_item.site_id
if auth_item.user_id_to_impersonate:
user_element = ET.SubElement(credentials_element, 'user')
user_element.attrib['id'] = auth_item.user_id_to_impersonate
@@ -30,12 +30,17 @@ def signin_req(self, auth_item):
class DatasourceRequest(object):
- def _generate_xml(self, datasource_item):
+ def _generate_xml(self, datasource_item, connection_credentials=None):
xml_request = ET.Element('tsRequest')
datasource_element = ET.SubElement(xml_request, 'datasource')
datasource_element.attrib['name'] = datasource_item.name
project_element = ET.SubElement(datasource_element, 'project')
project_element.attrib['id'] = datasource_item.project_id
+ if connection_credentials:
+ credentials_element = ET.SubElement(datasource_element, 'connectionCredentials')
+ credentials_element.attrib['name'] = connection_credentials.name
+ credentials_element.attrib['password'] = connection_credentials.password
+ credentials_element.attrib['embed'] = 'true' if connection_credentials.embed else 'false'
return ET.tostring(xml_request)
def update_req(self, datasource_item):
@@ -49,15 +54,15 @@ def update_req(self, datasource_item):
owner_element.attrib['id'] = datasource_item.owner_id
return ET.tostring(xml_request)
- def publish_req(self, datasource_item, filename, file_contents):
- xml_request = self._generate_xml(datasource_item)
+ def publish_req(self, datasource_item, filename, file_contents, connection_credentials=None):
+ xml_request = self._generate_xml(datasource_item, connection_credentials)
parts = {'request_payload': ('', xml_request, 'text/xml'),
'tableau_datasource': (filename, file_contents, 'application/octet-stream')}
return _add_multipart(parts)
- def publish_req_chunked(self, datasource_item):
- xml_request = self._generate_xml(datasource_item)
+ def publish_req_chunked(self, datasource_item, connection_credentials=None):
+ xml_request = self._generate_xml(datasource_item, connection_credentials)
parts = {'request_payload': ('', xml_request, 'text/xml')}
return _add_multipart(parts)
@@ -77,6 +82,12 @@ def add_user_req(self, user_id):
user_element.attrib['id'] = user_id
return ET.tostring(xml_request)
+ def create_req(self, group_item):
+ xml_request = ET.Element('tsRequest')
+ group_element = ET.SubElement(xml_request, 'group')
+ group_element.attrib['name'] = group_item.name
+ return ET.tostring(xml_request)
+
class PermissionRequest(object):
def _add_capability(self, parent_element, capability_set, mode):
@@ -129,6 +140,55 @@ def create_req(self, project_item):
return ET.tostring(xml_request)
+class ScheduleRequest(object):
+ def create_req(self, schedule_item):
+ xml_request = ET.Element('tsRequest')
+ schedule_element = ET.SubElement(xml_request, 'schedule')
+ schedule_element.attrib['name'] = schedule_item.name
+ schedule_element.attrib['priority'] = str(schedule_item.priority)
+ schedule_element.attrib['type'] = schedule_item.schedule_type
+ schedule_element.attrib['executionOrder'] = schedule_item.execution_order
+ interval_item = schedule_item.interval_item
+ schedule_element.attrib['frequency'] = interval_item._frequency
+ frequency_element = ET.SubElement(schedule_element, 'frequencyDetails')
+ frequency_element.attrib['start'] = str(interval_item.start_time)
+ if hasattr(interval_item, 'end_time') and interval_item.end_time:
+ frequency_element.attrib['end'] = str(interval_item.end_time)
+ if hasattr(interval_item, 'interval') and interval_item.interval:
+ intervals_element = ET.SubElement(frequency_element, 'intervals')
+ for interval in interval_item._interval_type_pairs():
+ expression, value = interval
+ single_interval_element = ET.SubElement(intervals_element, 'interval')
+ single_interval_element.attrib[expression] = value
+ return ET.tostring(xml_request)
+
+ def update_req(self, schedule_item):
+ xml_request = ET.Element('tsRequest')
+ schedule_element = ET.SubElement(xml_request, 'schedule')
+ if schedule_item.name:
+ schedule_element.attrib['name'] = schedule_item.name
+ if schedule_item.priority:
+ schedule_element.attrib['priority'] = str(schedule_item.priority)
+ if schedule_item.execution_order:
+ schedule_element.attrib['executionOrder'] = schedule_item.execution_order
+ if schedule_item.state:
+ schedule_element.attrib['state'] = schedule_item.state
+ interval_item = schedule_item.interval_item
+ if interval_item._frequency:
+ schedule_element.attrib['frequency'] = interval_item._frequency
+ frequency_element = ET.SubElement(schedule_element, 'frequencyDetails')
+ frequency_element.attrib['start'] = str(interval_item.start_time)
+ if hasattr(interval_item, 'end_time') and interval_item.end_time:
+ frequency_element.attrib['end'] = str(interval_item.end_time)
+ intervals_element = ET.SubElement(frequency_element, 'intervals')
+ if hasattr(interval_item, 'interval'):
+ for interval in interval_item._interval_type_pairs():
+ (expression, value) = interval
+ single_interval_element = ET.SubElement(intervals_element, 'interval')
+ single_interval_element.attrib[expression] = value
+ return ET.tostring(xml_request)
+
+
class SiteRequest(object):
def update_req(self, site_item):
xml_request = ET.Element('tsRequest')
@@ -178,7 +238,7 @@ def add_req(self, tag_set):
class UserRequest(object):
- def update_req(self, user_item, password=''):
+ def update_req(self, user_item, password):
xml_request = ET.Element('tsRequest')
user_element = ET.SubElement(xml_request, 'user')
if user_item.fullname:
@@ -205,7 +265,7 @@ def add_req(self, user_item):
class WorkbookRequest(object):
- def _generate_xml(self, workbook_item):
+ def _generate_xml(self, workbook_item, connection_credentials=None):
xml_request = ET.Element('tsRequest')
workbook_element = ET.SubElement(xml_request, 'workbook')
workbook_element.attrib['name'] = workbook_item.name
@@ -213,6 +273,11 @@ def _generate_xml(self, workbook_item):
workbook_element.attrib['showTabs'] = str(workbook_item.show_tabs).lower()
project_element = ET.SubElement(workbook_element, 'project')
project_element.attrib['id'] = workbook_item.project_id
+ if connection_credentials:
+ credentials_element = ET.SubElement(workbook_element, 'connectionCredentials')
+ credentials_element.attrib['name'] = connection_credentials.name
+ credentials_element.attrib['password'] = connection_credentials.password
+ credentials_element.attrib['embed'] = 'true' if connection_credentials.embed else 'false'
return ET.tostring(xml_request)
def update_req(self, workbook_item):
@@ -228,15 +293,15 @@ def update_req(self, workbook_item):
owner_element.attrib['id'] = workbook_item.owner_id
return ET.tostring(xml_request)
- def publish_req(self, workbook_item, filename, file_contents):
- xml_request = self._generate_xml(workbook_item)
+ def publish_req(self, workbook_item, filename, file_contents, connection_credentials=None):
+ xml_request = self._generate_xml(workbook_item, connection_credentials)
parts = {'request_payload': ('', xml_request, 'text/xml'),
'tableau_workbook': (filename, file_contents, 'application/octet-stream')}
return _add_multipart(parts)
- def publish_req_chunked(self, workbook_item):
- xml_request = self._generate_xml(workbook_item)
+ def publish_req_chunked(self, workbook_item, connection_credentials=None):
+ xml_request = self._generate_xml(workbook_item, connection_credentials)
parts = {'request_payload': ('', xml_request, 'text/xml')}
return _add_multipart(parts)
@@ -249,6 +314,7 @@ class RequestFactory(object):
Group = GroupRequest()
Permission = PermissionRequest()
Project = ProjectRequest()
+ Schedule = ScheduleRequest()
Site = SiteRequest()
Tag = TagRequest()
User = UserRequest()
diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py
index 3458e0644..2cb08a892 100644
--- a/tableauserverclient/server/server.py
+++ b/tableauserverclient/server/server.py
@@ -1,5 +1,6 @@
from .exceptions import NotSignedInError
-from .endpoint import Sites, Views, Users, Groups, Workbooks, Datasources, Projects, Auth
+from .endpoint import Sites, Views, Users, Groups, Workbooks, Datasources, Projects, Auth, Schedules, ServerInfo
+
import requests
@@ -26,6 +27,8 @@ def __init__(self, server_address):
self.workbooks = Workbooks(self)
self.datasources = Datasources(self)
self.projects = Projects(self)
+ self.schedules = Schedules(self)
+ self.server_info = ServerInfo(self)
def add_http_options(self, options_dict):
self._http_options.update(options_dict)
@@ -80,3 +83,6 @@ def http_options(self):
@property
def session(self):
return self._session
+
+ def is_signed_in(self):
+ return self._auth_token is not None
diff --git a/test/assets/group_create.xml b/test/assets/group_create.xml
new file mode 100644
index 000000000..8fb3902a4
--- /dev/null
+++ b/test/assets/group_create.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/group_create_async.xml b/test/assets/group_create_async.xml
new file mode 100644
index 000000000..8c7ac1c22
--- /dev/null
+++ b/test/assets/group_create_async.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/schedule_create_daily.xml b/test/assets/schedule_create_daily.xml
new file mode 100644
index 000000000..fe1eda485
--- /dev/null
+++ b/test/assets/schedule_create_daily.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/schedule_create_hourly.xml b/test/assets/schedule_create_hourly.xml
new file mode 100644
index 000000000..b1c3b73c3
--- /dev/null
+++ b/test/assets/schedule_create_hourly.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/schedule_create_monthly.xml b/test/assets/schedule_create_monthly.xml
new file mode 100644
index 000000000..408ff428d
--- /dev/null
+++ b/test/assets/schedule_create_monthly.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/schedule_create_weekly.xml b/test/assets/schedule_create_weekly.xml
new file mode 100644
index 000000000..624a56e25
--- /dev/null
+++ b/test/assets/schedule_create_weekly.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/schedule_get.xml b/test/assets/schedule_get.xml
new file mode 100644
index 000000000..3d8578ede
--- /dev/null
+++ b/test/assets/schedule_get.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/schedule_get_empty.xml b/test/assets/schedule_get_empty.xml
new file mode 100644
index 000000000..c40943303
--- /dev/null
+++ b/test/assets/schedule_get_empty.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/schedule_update.xml b/test/assets/schedule_update.xml
new file mode 100644
index 000000000..314925377
--- /dev/null
+++ b/test/assets/schedule_update.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/server_info_get.xml b/test/assets/server_info_get.xml
new file mode 100644
index 000000000..ce4e0b322
--- /dev/null
+++ b/test/assets/server_info_get.xml
@@ -0,0 +1,6 @@
+
+
+10.1.0
+2.4
+
+
\ No newline at end of file
diff --git a/test/assets/workbook_get_page_1.xml b/test/assets/workbook_get_page_1.xml
new file mode 100644
index 000000000..a5dfdcf89
--- /dev/null
+++ b/test/assets/workbook_get_page_1.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/assets/workbook_get_page_2.xml b/test/assets/workbook_get_page_2.xml
new file mode 100644
index 000000000..456cc1bcf
--- /dev/null
+++ b/test/assets/workbook_get_page_2.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/assets/workbook_get_page_3.xml b/test/assets/workbook_get_page_3.xml
new file mode 100644
index 000000000..e2fad1f2b
--- /dev/null
+++ b/test/assets/workbook_get_page_3.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/test/test_auth.py b/test/test_auth.py
index a833cead8..870064db0 100644
--- a/test/test_auth.py
+++ b/test/test_auth.py
@@ -20,7 +20,7 @@ def test_sign_in(self):
response_xml = f.read().decode('utf-8')
with requests_mock.mock() as m:
m.post(self.baseurl + '/signin', text=response_xml)
- tableau_auth = TSC.TableauAuth('testuser', 'password', site='Samples')
+ tableau_auth = TSC.TableauAuth('testuser', 'password', site_id='Samples')
self.server.auth.sign_in(tableau_auth)
self.assertEqual('eIX6mvFsqyansa4KqEI1UwOpS8ggRs2l', self.server.auth_token)
diff --git a/test/test_datasource_model.py b/test/test_datasource_model.py
new file mode 100644
index 000000000..b43cc3f3d
--- /dev/null
+++ b/test/test_datasource_model.py
@@ -0,0 +1,10 @@
+import unittest
+import tableauserverclient as TSC
+
+
+class DatasourceModelTests(unittest.TestCase):
+ def test_invalid_project_id(self):
+ self.assertRaises(ValueError, TSC.DatasourceItem, None)
+ datasource = TSC.DatasourceItem("10")
+ with self.assertRaises(ValueError):
+ datasource.project_id = None
diff --git a/test/test_group.py b/test/test_group.py
index a521c0d9a..ff928bf17 100644
--- a/test/test_group.py
+++ b/test/test_group.py
@@ -1,3 +1,4 @@
+# encoding=utf-8
import unittest
import os
import requests_mock
@@ -8,6 +9,8 @@
GET_XML = os.path.join(TEST_ASSET_DIR, 'group_get.xml')
POPULATE_USERS = os.path.join(TEST_ASSET_DIR, 'group_populate_users.xml')
ADD_USER = os.path.join(TEST_ASSET_DIR, 'group_add_user.xml')
+CREATE_GROUP = os.path.join(TEST_ASSET_DIR, 'group_create.xml')
+CREATE_GROUP_ASYNC = os.path.join(TEST_ASSET_DIR, 'group_create_async.xml')
class GroupTests(unittest.TestCase):
@@ -98,9 +101,17 @@ def test_add_user(self):
self.assertEqual('ServerAdministrator', user.site_role)
def test_add_user_before_populating(self):
- single_group = TSC.GroupItem('test')
- self.assertRaises(TSC.UnpopulatedPropertyError, self.server.groups.add_user, single_group,
- '5de011f8-5aa9-4d5b-b991-f462c8dd6bb7')
+ with open(GET_XML, 'rb') as f:
+ get_xml_response = f.read().decode('utf-8')
+ with open(ADD_USER, 'rb') as f:
+ add_user_response = f.read().decode('utf-8')
+ with requests_mock.mock() as m:
+ m.get(self.baseurl, text=get_xml_response)
+ m.post('http://test/api/2.3/sites/dad65087-b08b-4603-af4e-2887b8aafc67/groups/ef8b19c0-43b6-11e6-af50'
+ '-63f5805dbe3c/users', text=add_user_response)
+ all_groups, pagination_item = self.server.groups.get()
+ single_group = all_groups[0]
+ self.server.groups.add_user(single_group, '5de011f8-5aa9-4d5b-b991-f462c8dd6bb7')
def test_add_user_missing_user_id(self):
with open(POPULATE_USERS, 'rb') as f:
@@ -120,9 +131,16 @@ def test_add_user_missing_group_id(self):
'5de011f8-5aa9-4d5b-b991-f462c8dd6bb7')
def test_remove_user_before_populating(self):
- single_group = TSC.GroupItem('test')
- self.assertRaises(TSC.UnpopulatedPropertyError, self.server.groups.remove_user, single_group,
- '5de011f8-5aa9-4d5b-b991-f462c8dd6bb7')
+ with open(GET_XML, 'rb') as f:
+ response_xml = f.read().decode('utf-8')
+ with requests_mock.mock() as m:
+ m.get(self.baseurl, text=response_xml)
+ m.delete('http://test/api/2.3/sites/dad65087-b08b-4603-af4e-2887b8aafc67/groups/ef8b19c0-43b6-11e6-af50'
+ '-63f5805dbe3c/users/5de011f8-5aa9-4d5b-b991-f462c8dd6bb7',
+ text='ok')
+ all_groups, pagination_item = self.server.groups.get()
+ single_group = all_groups[0]
+ self.server.groups.remove_user(single_group, '5de011f8-5aa9-4d5b-b991-f462c8dd6bb7')
def test_remove_user_missing_user_id(self):
with open(POPULATE_USERS, 'rb') as f:
@@ -140,3 +158,13 @@ def test_remove_user_missing_group_id(self):
single_group._users = []
self.assertRaises(TSC.MissingRequiredFieldError, self.server.groups.remove_user, single_group,
'5de011f8-5aa9-4d5b-b991-f462c8dd6bb7')
+
+ def test_create_group(self):
+ with open(CREATE_GROUP, 'rb') as f:
+ response_xml = f.read().decode('utf-8')
+ with requests_mock.mock() as m:
+ m.post(self.baseurl, text=response_xml)
+ group_to_create = TSC.GroupItem(u'試供品')
+ group = self.server.groups.create(group_to_create)
+ self.assertEqual(group.name, u'試供品')
+ self.assertEqual(group.id, '3e4a9ea0-a07a-4fe6-b50f-c345c8c81034')
diff --git a/test/test_group_model.py b/test/test_group_model.py
new file mode 100644
index 000000000..eb11adcdd
--- /dev/null
+++ b/test/test_group_model.py
@@ -0,0 +1,14 @@
+import unittest
+import tableauserverclient as TSC
+
+
+class GroupModelTests(unittest.TestCase):
+ def test_invalid_name(self):
+ self.assertRaises(ValueError, TSC.GroupItem, None)
+ self.assertRaises(ValueError, TSC.GroupItem, "")
+ group = TSC.GroupItem("grp")
+ with self.assertRaises(ValueError):
+ group.name = None
+
+ with self.assertRaises(ValueError):
+ group.name = ""
diff --git a/test/test_pager.py b/test/test_pager.py
new file mode 100644
index 000000000..e3cec1ce8
--- /dev/null
+++ b/test/test_pager.py
@@ -0,0 +1,88 @@
+import unittest
+import os
+import requests_mock
+import tableauserverclient as TSC
+
+TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets')
+
+GET_XML_PAGE1 = os.path.join(TEST_ASSET_DIR, 'workbook_get_page_1.xml')
+GET_XML_PAGE2 = os.path.join(TEST_ASSET_DIR, 'workbook_get_page_2.xml')
+GET_XML_PAGE3 = os.path.join(TEST_ASSET_DIR, 'workbook_get_page_3.xml')
+
+
+class PagerTests(unittest.TestCase):
+ def setUp(self):
+ self.server = TSC.Server('http://test')
+
+ # Fake sign in
+ self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67'
+ self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM'
+
+ self.baseurl = self.server.workbooks.baseurl
+
+ def test_pager_with_no_options(self):
+ with open(GET_XML_PAGE1, 'rb') as f:
+ page_1 = f.read().decode('utf-8')
+ with open(GET_XML_PAGE2, 'rb') as f:
+ page_2 = f.read().decode('utf-8')
+ with open(GET_XML_PAGE3, 'rb') as f:
+ page_3 = f.read().decode('utf-8')
+ with requests_mock.mock() as m:
+ # Register Pager with default request options
+ m.get(self.baseurl, text=page_1)
+
+ # Register Pager with some pages
+ m.get(self.baseurl + "?pageNumber=1&pageSize=1", text=page_1)
+ m.get(self.baseurl + "?pageNumber=2&pageSize=1", text=page_2)
+ m.get(self.baseurl + "?pageNumber=3&pageSize=1", text=page_3)
+
+ # No options should get all 3
+ workbooks = list(TSC.Pager(self.server.workbooks))
+ self.assertTrue(len(workbooks) == 3)
+
+ # Let's check that workbook items aren't duplicates
+ wb1, wb2, wb3 = workbooks
+ self.assertEqual(wb1.name, 'Page1Workbook')
+ self.assertEqual(wb2.name, 'Page2Workbook')
+ self.assertEqual(wb3.name, 'Page3Workbook')
+
+ def test_pager_with_options(self):
+ with open(GET_XML_PAGE1, 'rb') as f:
+ page_1 = f.read().decode('utf-8')
+ with open(GET_XML_PAGE2, 'rb') as f:
+ page_2 = f.read().decode('utf-8')
+ with open(GET_XML_PAGE3, 'rb') as f:
+ page_3 = f.read().decode('utf-8')
+ with requests_mock.mock() as m:
+ # Register Pager with some pages
+ m.get(self.baseurl + "?pageNumber=1&pageSize=1", text=page_1)
+ m.get(self.baseurl + "?pageNumber=2&pageSize=1", text=page_2)
+ m.get(self.baseurl + "?pageNumber=3&pageSize=1", text=page_3)
+ m.get(self.baseurl + "?pageNumber=1&pageSize=3", text=page_1)
+
+ # Starting on page 2 should get 2 out of 3
+ opts = TSC.RequestOptions(2, 1)
+ workbooks = list(TSC.Pager(self.server.workbooks, opts))
+ self.assertTrue(len(workbooks) == 2)
+
+ # Check that the workbooks are the 2 we think they should be
+ wb2, wb3 = workbooks
+ self.assertEqual(wb2.name, 'Page2Workbook')
+ self.assertEqual(wb3.name, 'Page3Workbook')
+
+ # Starting on 1 with pagesize of 3 should get all 3
+ opts = TSC.RequestOptions(1, 3)
+ workbooks = list(TSC.Pager(self.server.workbooks, opts))
+ self.assertTrue(len(workbooks) == 3)
+ wb1, wb2, wb3 = workbooks
+ self.assertEqual(wb1.name, 'Page1Workbook')
+ self.assertEqual(wb2.name, 'Page2Workbook')
+ self.assertEqual(wb3.name, 'Page3Workbook')
+
+ # Starting on 3 with pagesize of 1 should get the last item
+ opts = TSC.RequestOptions(3, 1)
+ workbooks = list(TSC.Pager(self.server.workbooks, opts))
+ self.assertTrue(len(workbooks) == 1)
+ # Should have the last workbook
+ wb3 = workbooks.pop()
+ self.assertEqual(wb3.name, 'Page3Workbook')
diff --git a/test/test_project_model.py b/test/test_project_model.py
new file mode 100644
index 000000000..3ab14b3f6
--- /dev/null
+++ b/test/test_project_model.py
@@ -0,0 +1,19 @@
+import unittest
+import tableauserverclient as TSC
+
+
+class ProjectModelTests(unittest.TestCase):
+ def test_invalid_name(self):
+ self.assertRaises(ValueError, TSC.ProjectItem, None)
+ self.assertRaises(ValueError, TSC.ProjectItem, "")
+ project = TSC.ProjectItem("proj")
+ with self.assertRaises(ValueError):
+ project.name = None
+
+ with self.assertRaises(ValueError):
+ project.name = ""
+
+ def test_invalid_content_permissions(self):
+ project = TSC.ProjectItem("proj")
+ with self.assertRaises(ValueError):
+ project.content_permissions = "Hello"
diff --git a/test/test_schedule.py b/test/test_schedule.py
new file mode 100644
index 000000000..710bfe2a2
--- /dev/null
+++ b/test/test_schedule.py
@@ -0,0 +1,183 @@
+import unittest
+import os
+import requests_mock
+import tableauserverclient as TSC
+from datetime import time
+
+TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets")
+
+GET_XML = os.path.join(TEST_ASSET_DIR, "schedule_get.xml")
+GET_EMPTY_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_empty.xml")
+CREATE_HOURLY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_hourly.xml")
+CREATE_DAILY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_daily.xml")
+CREATE_WEEKLY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_weekly.xml")
+CREATE_MONTHLY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_monthly.xml")
+UPDATE_XML = os.path.join(TEST_ASSET_DIR, "schedule_update.xml")
+
+
+class ScheduleTests(unittest.TestCase):
+ def setUp(self):
+ self.server = TSC.Server("http://test")
+
+ # Fake Signin
+ self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67"
+ self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM"
+
+ self.baseurl = self.server.schedules.baseurl
+
+ def test_get(self):
+ with open(GET_XML, "rb") as f:
+ response_xml = f.read().decode("utf-8")
+ with requests_mock.mock() as m:
+ m.get(self.baseurl, text=response_xml)
+ all_schedules, pagination_item = self.server.schedules.get()
+
+ self.assertEqual(2, pagination_item.total_available)
+ self.assertEqual("c9cff7f9-309c-4361-99ff-d4ba8c9f5467", all_schedules[0].id)
+ self.assertEqual("Weekday early mornings", all_schedules[0].name)
+ self.assertEqual("Active", all_schedules[0].state)
+ self.assertEqual(50, all_schedules[0].priority)
+ self.assertEqual("2016-07-06T20:19:00Z", all_schedules[0].created_at)
+ self.assertEqual("2016-09-13T11:00:32Z", all_schedules[0].updated_at)
+ self.assertEqual("Extract", all_schedules[0].schedule_type)
+ self.assertEqual("2016-09-14T11:00:00Z", all_schedules[0].next_run_at)
+
+ self.assertEqual("bcb79d07-6e47-472f-8a65-d7f51f40c36c", all_schedules[1].id)
+ self.assertEqual("Saturday night", all_schedules[1].name)
+ self.assertEqual("Active", all_schedules[1].state)
+ self.assertEqual(80, all_schedules[1].priority)
+ self.assertEqual("2016-07-07T20:19:00Z", all_schedules[1].created_at)
+ self.assertEqual("2016-09-12T16:39:38Z", all_schedules[1].updated_at)
+ self.assertEqual("Subscription", all_schedules[1].schedule_type)
+ self.assertEqual("2016-09-18T06:00:00Z", all_schedules[1].next_run_at)
+
+ def test_get_empty(self):
+ with open(GET_EMPTY_XML, "rb") as f:
+ response_xml = f.read().decode("utf-8")
+ with requests_mock.mock() as m:
+ m.get(self.baseurl, text=response_xml)
+ all_schedules, pagination_item = self.server.schedules.get()
+
+ self.assertEqual(0, pagination_item.total_available)
+ self.assertEqual([], all_schedules)
+
+ def test_delete(self):
+ with requests_mock.mock() as m:
+ m.delete(self.baseurl + "/c9cff7f9-309c-4361-99ff-d4ba8c9f5467", status_code=204)
+ self.server.schedules.delete("c9cff7f9-309c-4361-99ff-d4ba8c9f5467")
+
+ def test_create_hourly(self):
+ with open(CREATE_HOURLY_XML, "rb") as f:
+ response_xml = f.read().decode("utf-8")
+ with requests_mock.mock() as m:
+ m.post(self.baseurl, text=response_xml)
+ hourly_interval = TSC.HourlyInterval(start_time=time(2, 30),
+ end_time=time(23, 0),
+ interval_value=2)
+ new_schedule = TSC.ScheduleItem("hourly-schedule-1", 50, TSC.ScheduleItem.Type.Extract,
+ TSC.ScheduleItem.ExecutionOrder.Parallel, hourly_interval)
+ new_schedule = self.server.schedules.create(new_schedule)
+
+ self.assertEqual("5f42be25-8a43-47ba-971a-63f2d4e7029c", new_schedule.id)
+ self.assertEqual("hourly-schedule-1", new_schedule.name)
+ self.assertEqual("Active", new_schedule.state)
+ self.assertEqual(50, new_schedule.priority)
+ self.assertEqual("2016-09-15T20:47:33Z", new_schedule.created_at)
+ self.assertEqual("2016-09-15T20:47:33Z", new_schedule.updated_at)
+ self.assertEqual(TSC.ScheduleItem.Type.Extract, new_schedule.schedule_type)
+ self.assertEqual("2016-09-16T01:30:00Z", new_schedule.next_run_at)
+ self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Parallel, new_schedule.execution_order)
+ self.assertEqual(time(2, 30), new_schedule.interval_item.start_time)
+ self.assertEqual(time(23), new_schedule.interval_item.end_time)
+ self.assertEqual("8", new_schedule.interval_item.interval)
+
+ def test_create_daily(self):
+ with open(CREATE_DAILY_XML, "rb") as f:
+ response_xml = f.read().decode("utf-8")
+ with requests_mock.mock() as m:
+ m.post(self.baseurl, text=response_xml)
+ daily_interval = TSC.DailyInterval(time(4, 50))
+ new_schedule = TSC.ScheduleItem("daily-schedule-1", 90, TSC.ScheduleItem.Type.Subscription,
+ TSC.ScheduleItem.ExecutionOrder.Serial, daily_interval)
+ new_schedule = self.server.schedules.create(new_schedule)
+
+ self.assertEqual("907cae38-72fd-417c-892a-95540c4664cd", new_schedule.id)
+ self.assertEqual("daily-schedule-1", new_schedule.name)
+ self.assertEqual("Active", new_schedule.state)
+ self.assertEqual(90, new_schedule.priority)
+ self.assertEqual("2016-09-15T21:01:09Z", new_schedule.created_at)
+ self.assertEqual("2016-09-15T21:01:09Z", new_schedule.updated_at)
+ self.assertEqual(TSC.ScheduleItem.Type.Subscription, new_schedule.schedule_type)
+ self.assertEqual("2016-09-16T11:45:00Z", new_schedule.next_run_at)
+ self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Serial, new_schedule.execution_order)
+ self.assertEqual(time(4, 45), new_schedule.interval_item.start_time)
+
+ def test_create_weekly(self):
+ with open(CREATE_WEEKLY_XML, "rb") as f:
+ response_xml = f.read().decode("utf-8")
+ with requests_mock.mock() as m:
+ m.post(self.baseurl, text=response_xml)
+ weekly_interval = TSC.WeeklyInterval(time(9, 15), TSC.IntervalItem.Day.Monday,
+ TSC.IntervalItem.Day.Wednesday,
+ TSC.IntervalItem.Day.Friday)
+ new_schedule = TSC.ScheduleItem("weekly-schedule-1", 80, TSC.ScheduleItem.Type.Extract,
+ TSC.ScheduleItem.ExecutionOrder.Parallel, weekly_interval)
+ new_schedule = self.server.schedules.create(new_schedule)
+
+ self.assertEqual("1adff386-6be0-4958-9f81-a35e676932bf", new_schedule.id)
+ self.assertEqual("weekly-schedule-1", new_schedule.name)
+ self.assertEqual("Active", new_schedule.state)
+ self.assertEqual(80, new_schedule.priority)
+ self.assertEqual("2016-09-15T21:12:50Z", new_schedule.created_at)
+ self.assertEqual("2016-09-15T21:12:50Z", new_schedule.updated_at)
+ self.assertEqual(TSC.ScheduleItem.Type.Extract, new_schedule.schedule_type)
+ self.assertEqual("2016-09-16T16:15:00Z", new_schedule.next_run_at)
+ self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Parallel, new_schedule.execution_order)
+ self.assertEqual(time(9, 15), new_schedule.interval_item.start_time)
+ self.assertEqual(("Monday", "Wednesday", "Friday"),
+ new_schedule.interval_item.interval)
+
+ def test_create_monthly(self):
+ with open(CREATE_MONTHLY_XML, "rb") as f:
+ response_xml = f.read().decode("utf-8")
+ with requests_mock.mock() as m:
+ m.post(self.baseurl, text=response_xml)
+ monthly_interval = TSC.MonthlyInterval(time(7), 12)
+ new_schedule = TSC.ScheduleItem("monthly-schedule-1", 20, TSC.ScheduleItem.Type.Extract,
+ TSC.ScheduleItem.ExecutionOrder.Serial, monthly_interval)
+ new_schedule = self.server.schedules.create(new_schedule)
+
+ self.assertEqual("e06a7c75-5576-4f68-882d-8909d0219326", new_schedule.id)
+ self.assertEqual("monthly-schedule-1", new_schedule.name)
+ self.assertEqual("Active", new_schedule.state)
+ self.assertEqual(20, new_schedule.priority)
+ self.assertEqual("2016-09-15T21:16:56Z", new_schedule.created_at)
+ self.assertEqual("2016-09-15T21:16:56Z", new_schedule.updated_at)
+ self.assertEqual(TSC.ScheduleItem.Type.Extract, new_schedule.schedule_type)
+ self.assertEqual("2016-10-12T14:00:00Z", new_schedule.next_run_at)
+ self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Serial, new_schedule.execution_order)
+ self.assertEqual(time(7), new_schedule.interval_item.start_time)
+ self.assertEqual("12", new_schedule.interval_item.interval)
+
+ def test_update(self):
+ with open(UPDATE_XML, "rb") as f:
+ response_xml = f.read().decode("utf-8")
+ with requests_mock.mock() as m:
+ m.put(self.baseurl + '/7bea1766-1543-4052-9753-9d224bc069b5', text=response_xml)
+ new_interval = TSC.WeeklyInterval(time(7), TSC.IntervalItem.Day.Monday,
+ TSC.IntervalItem.Day.Friday)
+ single_schedule = TSC.ScheduleItem("weekly-schedule-1", 90, TSC.ScheduleItem.Type.Extract,
+ TSC.ScheduleItem.ExecutionOrder.Parallel, new_interval)
+ single_schedule._id = "7bea1766-1543-4052-9753-9d224bc069b5"
+ single_schedule = self.server.schedules.update(single_schedule)
+
+ self.assertEqual("7bea1766-1543-4052-9753-9d224bc069b5", single_schedule.id)
+ self.assertEqual("weekly-schedule-1", single_schedule.name)
+ self.assertEqual(90, single_schedule.priority)
+ self.assertEqual("2016-09-15T23:50:02Z", single_schedule.updated_at)
+ self.assertEqual(TSC.ScheduleItem.Type.Extract, single_schedule.schedule_type)
+ self.assertEqual("2016-09-16T14:00:00Z", single_schedule.next_run_at)
+ self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Parallel, single_schedule.execution_order)
+ self.assertEqual(time(7), single_schedule.interval_item.start_time)
+ self.assertEqual(("Monday", "Friday"),
+ single_schedule.interval_item.interval)
diff --git a/test/test_server_info.py b/test/test_server_info.py
new file mode 100644
index 000000000..03e39210f
--- /dev/null
+++ b/test/test_server_info.py
@@ -0,0 +1,26 @@
+import unittest
+import os.path
+import requests_mock
+import tableauserverclient as TSC
+
+TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets')
+
+SERVER_INFO_GET_XML = os.path.join(TEST_ASSET_DIR, 'server_info_get.xml')
+
+
+class ServerInfoTests(unittest.TestCase):
+ def setUp(self):
+ self.server = TSC.Server('http://test')
+ self.server.version = '2.4'
+ self.baseurl = self.server.server_info.baseurl
+
+ def test_server_info_get(self):
+ with open(SERVER_INFO_GET_XML, 'rb') as f:
+ response_xml = f.read().decode('utf-8')
+ with requests_mock.mock() as m:
+ m.get(self.baseurl, text=response_xml)
+ actual = self.server.server_info.get()
+
+ self.assertEqual('10.1.0', actual.product_version)
+ self.assertEqual('10100.16.1024.2100', actual.build_number)
+ self.assertEqual('2.4', actual.rest_api_version)
diff --git a/test/test_site.py b/test/test_site.py
index 3076e4ce3..311f9524f 100644
--- a/test/test_site.py
+++ b/test/test_site.py
@@ -17,7 +17,7 @@ def setUp(self):
# Fake signin
self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM'
-
+ self.server._site_id = '0626857c-1def-4503-a7d8-7907c3ff9d9f'
self.baseurl = self.server.sites.baseurl
def test_get(self):
diff --git a/test/test_site_model.py b/test/test_site_model.py
new file mode 100644
index 000000000..99fa73ce9
--- /dev/null
+++ b/test/test_site_model.py
@@ -0,0 +1,67 @@
+# coding=utf-8
+
+import unittest
+import tableauserverclient as TSC
+
+
+class SiteModelTests(unittest.TestCase):
+ def test_invalid_name(self):
+ self.assertRaises(ValueError, TSC.SiteItem, None, "url")
+ self.assertRaises(ValueError, TSC.SiteItem, "", "url")
+ site = TSC.SiteItem("site", "url")
+ with self.assertRaises(ValueError):
+ site.name = None
+
+ with self.assertRaises(ValueError):
+ site.name = ""
+
+ def test_invalid_admin_mode(self):
+ site = TSC.SiteItem("site", "url")
+ with self.assertRaises(ValueError):
+ site.admin_mode = "Hello"
+
+ def test_invalid_content_url(self):
+
+ with self.assertRaises(ValueError):
+ site = TSC.SiteItem(name="蚵仔煎", content_url="蚵仔煎")
+
+ with self.assertRaises(ValueError):
+ site = TSC.SiteItem(name="蚵仔煎", content_url=None)
+
+ def test_set_valid_content_url(self):
+ # Default Site
+ site = TSC.SiteItem(name="Default", content_url="")
+ self.assertEqual(site.content_url, "")
+
+ # Unicode Name and ascii content_url
+ site = TSC.SiteItem(name="蚵仔煎", content_url="omlette")
+ self.assertEqual(site.content_url, "omlette")
+
+ def test_invalid_disable_subscriptions(self):
+ site = TSC.SiteItem("site", "url")
+ with self.assertRaises(ValueError):
+ site.disable_subscriptions = "Hello"
+
+ with self.assertRaises(ValueError):
+ site.disable_subscriptions = None
+
+ def test_invalid_revision_history_enabled(self):
+ site = TSC.SiteItem("site", "url")
+ with self.assertRaises(ValueError):
+ site.revision_history_enabled = "Hello"
+
+ with self.assertRaises(ValueError):
+ site.revision_history_enabled = None
+
+ def test_invalid_state(self):
+ site = TSC.SiteItem("site", "url")
+ with self.assertRaises(ValueError):
+ site.state = "Hello"
+
+ def test_invalid_subscribe_others_enabled(self):
+ site = TSC.SiteItem("site", "url")
+ with self.assertRaises(ValueError):
+ site.subscribe_others_enabled = "Hello"
+
+ with self.assertRaises(ValueError):
+ site.subscribe_others_enabled = None
diff --git a/test/test_user.py b/test/test_user.py
index ae0357cbe..71ec30207 100644
--- a/test/test_user.py
+++ b/test/test_user.py
@@ -87,7 +87,6 @@ def test_update(self):
single_user.name = 'Cassie'
single_user.fullname = 'Cassie'
single_user.email = 'cassie@email.com'
- single_user.password = 'password'
single_user = self.server.users.update(single_user)
self.assertEqual('Cassie', single_user.name)
diff --git a/test/test_user_model.py b/test/test_user_model.py
new file mode 100644
index 000000000..5826fb148
--- /dev/null
+++ b/test/test_user_model.py
@@ -0,0 +1,24 @@
+import unittest
+import tableauserverclient as TSC
+
+
+class UserModelTests(unittest.TestCase):
+ def test_invalid_name(self):
+ self.assertRaises(ValueError, TSC.UserItem, None, TSC.UserItem.Roles.Publisher)
+ self.assertRaises(ValueError, TSC.UserItem, "", TSC.UserItem.Roles.Publisher)
+ user = TSC.UserItem("me", TSC.UserItem.Roles.Publisher)
+ with self.assertRaises(ValueError):
+ user.name = None
+
+ with self.assertRaises(ValueError):
+ user.name = ""
+
+ def test_invalid_auth_setting(self):
+ user = TSC.UserItem("me", TSC.UserItem.Roles.Publisher)
+ with self.assertRaises(ValueError):
+ user.auth_setting = "Hello"
+
+ def test_invalid_site_role(self):
+ user = TSC.UserItem("me", TSC.UserItem.Roles.Publisher)
+ with self.assertRaises(ValueError):
+ user.site_role = "Hello"
diff --git a/test/test_workbook_model.py b/test/test_workbook_model.py
new file mode 100644
index 000000000..69188fa4a
--- /dev/null
+++ b/test/test_workbook_model.py
@@ -0,0 +1,18 @@
+import unittest
+import tableauserverclient as TSC
+
+
+class WorkbookModelTests(unittest.TestCase):
+ def test_invalid_project_id(self):
+ self.assertRaises(ValueError, TSC.WorkbookItem, None)
+ workbook = TSC.WorkbookItem("10")
+ with self.assertRaises(ValueError):
+ workbook.project_id = None
+
+ def test_invalid_show_tabs(self):
+ workbook = TSC.WorkbookItem("10")
+ with self.assertRaises(ValueError):
+ workbook.show_tabs = "Hello"
+
+ with self.assertRaises(ValueError):
+ workbook.show_tabs = None