diff --git a/.travis.yml b/.travis.yml index 68cee02ad..41316d700 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: - "3.5" - "3.6" - "3.7" + - "3.8" # command to install dependencies install: - "pip install -e ." diff --git a/README.md b/README.md index 51e23549a..e2c30704a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Tableau Server Client (Python) -[![Tableau Supported](https://img.shields.io/badge/Support%20Level-Tableau%20Supported-53bd92.svg)](https://www.tableau.com/support-levels-it-and-developer-tools) + +[![Tableau Supported](https://img.shields.io/badge/Support%20Level-Tableau%20Supported-53bd92.svg)](https://www.tableau.com/support-levels-it-and-developer-tools) [![Build Status](https://travis-ci.org/tableau/server-client-python.svg?branch=master)](https://travis-ci.org/tableau/server-client-python) Use the Tableau Server Client (TSC) library to increase your productivity as you interact with the Tableau Server REST API. With the TSC library you can do almost everything that you can do with the REST API, including: @@ -7,8 +8,7 @@ Use the Tableau Server Client (TSC) library to increase your productivity as you * Create users and groups. * Query projects, sites, and more. -This repository contains Python source code and sample files. +This repository contains Python source code and sample files. Python versions 3.5 and up are supported. For more information on installing and using TSC, see the documentation: - diff --git a/contributing.md b/contributing.md index 4c7cdef00..c7f487ec3 100644 --- a/contributing.md +++ b/contributing.md @@ -15,7 +15,7 @@ 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. +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.** @@ -48,19 +48,24 @@ anyone can add to an 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 [Development Guide](https://tableau.github.io/server-client-python/docs/dev-guide) +creating a PR can be found in the [Development Guide](https://tableau.github.io/server-client-python/docs/dev-guide). If the feature is complex or has multiple solutions that could be equally appropriate approaches, it would be helpful to file an issue to discuss the design trade-offs of each solution before implementing, to allow us to collectively arrive at the best solution, which most likely exists in the middle somewhere. - ## Getting Started -> pip install versioneer -> python setup.py build -> python setup.py test -> - -### before committing -Our CI runs include a python lint run, so you should run this locally and fix complaints before committing as this will fail your checkin -> pycodestyle tableauserverclient test samples + +```shell +pip install versioneer +python setup.py build +python setup.py test +``` + +### Before Committing + +Our CI runs include a Python lint run, so you should run this locally and fix complaints before committing as this will fail your checkin. + +```shell +pycodestyle tableauserverclient test samples +``` diff --git a/samples/add_default_permission.py b/samples/add_default_permission.py index b6dbdd479..63c38f53d 100644 --- a/samples/add_default_permission.py +++ b/samples/add_default_permission.py @@ -17,7 +17,7 @@ def main(): - parser = argparse.ArgumentParser(description='Add workbook default permission for a given project') + parser = argparse.ArgumentParser(description='Add workbook default permissions for a given project.') parser.add_argument('--server', '-s', required=True, help='Server address') parser.add_argument('--username', '-u', required=True, help='Username to sign into server') parser.add_argument('--site', '-S', default=None, help='Site to sign into - default site if not provided') diff --git a/samples/create_group.py b/samples/create_group.py index c6865bc56..7f9dc1e96 100644 --- a/samples/create_group.py +++ b/samples/create_group.py @@ -1,5 +1,5 @@ #### -# This script demonstrates how to create groups using the Tableau +# This script demonstrates how to create a group using the Tableau # Server Client. # # To run the script, you must have installed Python 3.5 or later. @@ -17,7 +17,7 @@ def main(): - parser = argparse.ArgumentParser(description='Creates sample schedules for each type of frequency.') + parser = argparse.ArgumentParser(description='Creates a sample user group.') 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', diff --git a/samples/create_project.py b/samples/create_project.py index ac55da17e..0380cb8a0 100644 --- a/samples/create_project.py +++ b/samples/create_project.py @@ -26,7 +26,7 @@ def create_project(server, project_item): def main(): - parser = argparse.ArgumentParser(description='Get all of the refresh tasks available on a server') + parser = argparse.ArgumentParser(description='Create new projects.') parser.add_argument('--server', '-s', required=True, help='server address') parser.add_argument('--username', '-u', required=True, help='username to sign into server') parser.add_argument('--site', '-S', default=None) diff --git a/samples/download_view_image.py b/samples/download_view_image.py index ce6dd3165..07162eebf 100644 --- a/samples/download_view_image.py +++ b/samples/download_view_image.py @@ -17,7 +17,7 @@ def main(): - parser = argparse.ArgumentParser(description='Query View Image From Server') + parser = argparse.ArgumentParser(description='Download image of a specified view.') parser.add_argument('--server', '-s', required=True, help='server address') parser.add_argument('--site-id', '-si', required=False, help='content url for site the view is on') diff --git a/samples/export.py b/samples/export.py index 67b3319a8..b8cd01140 100644 --- a/samples/export.py +++ b/samples/export.py @@ -1,3 +1,10 @@ +#### +# This script demonstrates how to export a view using the Tableau +# Server Client. +# +# To run the script, you must have installed Python 3.5 or later. +#### + import argparse import getpass import logging @@ -6,7 +13,7 @@ def main(): - parser = argparse.ArgumentParser(description='Export a view as an image, pdf, or csv') + parser = argparse.ArgumentParser(description='Export a view as an image, PDF, or CSV') parser.add_argument('--server', '-s', required=True, help='server address') parser.add_argument('--username', '-u', required=True, help='username to sign into server') parser.add_argument('--site', '-S', default=None) diff --git a/samples/export_wb.py b/samples/export_wb.py index 8d3640ab4..334d57c89 100644 --- a/samples/export_wb.py +++ b/samples/export_wb.py @@ -1,9 +1,12 @@ -# +#### # This sample uses the PyPDF2 library for combining pdfs together to get the full pdf for all the views in a # workbook. # # You will need to do `pip install PyPDF2` to use this sample. # +# To run the script, you must have installed Python 3.5 or later. +#### + import argparse import getpass @@ -48,7 +51,7 @@ def cleanup(tempdir): def main(): - parser = argparse.ArgumentParser(description='Export to PDF all of the views in a workbook') + parser = argparse.ArgumentParser(description='Export to PDF all of the views in a workbook.') parser.add_argument('--server', '-s', required=True, help='server address') parser.add_argument('--site', '-S', default=None, help='Site to log into, do not specify for default site') parser.add_argument('--username', '-u', required=True, help='username to sign into server') diff --git a/samples/filter_sort_groups.py b/samples/filter_sort_groups.py index fa0c2318e..f8123a29c 100644 --- a/samples/filter_sort_groups.py +++ b/samples/filter_sort_groups.py @@ -1,5 +1,5 @@ #### -# This script demonstrates how to filter groups using the Tableau +# This script demonstrates how to filter and sort groups using the Tableau # Server Client. # # To run the script, you must have installed Python 3.5 or later. @@ -24,7 +24,7 @@ def create_example_group(group_name='Example Group', server=None): def main(): - parser = argparse.ArgumentParser(description='Filter on groups') + parser = argparse.ArgumentParser(description='Filter and sort groups.') 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', diff --git a/samples/filter_sort_projects.py b/samples/filter_sort_projects.py index 91633f38f..0c62614b0 100644 --- a/samples/filter_sort_projects.py +++ b/samples/filter_sort_projects.py @@ -2,7 +2,6 @@ # This script demonstrates how to use the Tableau Server Client # to filter and sort on the name of the projects present on site. # -# # To run the script, you must have installed Python 3.5 or later. #### @@ -26,7 +25,7 @@ def create_example_project(name='Example Project', content_permissions='LockedTo def main(): - parser = argparse.ArgumentParser(description='Get all of the refresh tasks available on a server') + parser = argparse.ArgumentParser(description='Filter and sort projects.') parser.add_argument('--server', '-s', required=True, help='server address') parser.add_argument('--username', '-u', required=True, help='username to sign into server') parser.add_argument('--site', '-S', default=None) diff --git a/samples/kill_all_jobs.py b/samples/kill_all_jobs.py index 9d3c7836a..1aeb7298e 100644 --- a/samples/kill_all_jobs.py +++ b/samples/kill_all_jobs.py @@ -12,7 +12,7 @@ def main(): - parser = argparse.ArgumentParser(description='Cancel all of the running background jobs') + parser = argparse.ArgumentParser(description='Cancel all of the running background jobs.') parser.add_argument('--server', '-s', required=True, help='server address') parser.add_argument('--site', '-S', default=None, help='site to log into, do not specify for default site') parser.add_argument('--username', '-u', required=True, help='username to sign into server') diff --git a/samples/list.py b/samples/list.py index 84b3c70d2..10e11ac04 100644 --- a/samples/list.py +++ b/samples/list.py @@ -14,7 +14,7 @@ def main(): - parser = argparse.ArgumentParser(description='List out the names and LUIDs for different resource types') + parser = argparse.ArgumentParser(description='List out the names and LUIDs for different resource types.') parser.add_argument('--server', '-s', required=True, help='server address') parser.add_argument('--site', '-S', default="", help='site to log into, do not specify for default site') parser.add_argument('--token-name', '-n', required=True, help='username to signin under') diff --git a/samples/pagination_sample.py b/samples/pagination_sample.py index 25effd7b2..6779023ba 100644 --- a/samples/pagination_sample.py +++ b/samples/pagination_sample.py @@ -19,7 +19,7 @@ def main(): - parser = argparse.ArgumentParser(description='Return a list of all of the workbooks on your server') + parser = argparse.ArgumentParser(description='Demonstrate pagination on the list of workbooks on the 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', diff --git a/samples/query_permissions.py b/samples/query_permissions.py index 48120f398..a253adc9a 100644 --- a/samples/query_permissions.py +++ b/samples/query_permissions.py @@ -14,7 +14,7 @@ def main(): - parser = argparse.ArgumentParser(description='Query permissions of a given resource') + parser = argparse.ArgumentParser(description='Query permissions of a given resource.') parser.add_argument('--server', '-s', required=True, help='Server address') parser.add_argument('--username', '-u', required=True, help='Username to sign into server') parser.add_argument('--site', '-S', default=None, help='Site to sign into - default site if not provided') diff --git a/samples/refresh.py b/samples/refresh.py index ba3a2f183..96937a6e3 100644 --- a/samples/refresh.py +++ b/samples/refresh.py @@ -12,7 +12,7 @@ def main(): - parser = argparse.ArgumentParser(description='Get all of the refresh tasks available on a server') + parser = argparse.ArgumentParser(description='Trigger a refresh task on a workbook or datasource.') parser.add_argument('--server', '-s', required=True, help='server address') parser.add_argument('--username', '-u', required=True, help='username to sign into server') parser.add_argument('--site', '-S', default=None) diff --git a/samples/set_refresh_schedule.py b/samples/set_refresh_schedule.py index edb94f47e..2d4761560 100644 --- a/samples/set_refresh_schedule.py +++ b/samples/set_refresh_schedule.py @@ -1,3 +1,11 @@ +#### +# This script demonstrates how to set the refresh schedule for +# a workbook or datasource. +# +# To run the script, you must have installed Python 3.5 or later. +#### + + import argparse import getpass import logging @@ -6,7 +14,7 @@ def usage(args): - parser = argparse.ArgumentParser(description='Explore workbook functions supported by the Server API.') + parser = argparse.ArgumentParser(description='Set refresh schedule for a workbook or datasource.') 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', diff --git a/tableauserverclient/models/job_item.py b/tableauserverclient/models/job_item.py index 58d1f1396..cc8b7df43 100644 --- a/tableauserverclient/models/job_item.py +++ b/tableauserverclient/models/job_item.py @@ -3,7 +3,8 @@ class JobItem(object): - def __init__(self, id_, job_type, progress, created_at, started_at=None, completed_at=None, finish_code=0): + def __init__(self, id_, job_type, progress, created_at, started_at=None, + completed_at=None, finish_code=0, notes=None): self._id = id_ self._type = job_type self._progress = progress @@ -11,6 +12,7 @@ def __init__(self, id_, job_type, progress, created_at, started_at=None, complet self._started_at = started_at self._completed_at = completed_at self._finish_code = finish_code + self._notes = notes or [] @property def id(self): @@ -40,6 +42,10 @@ def completed_at(self): def finish_code(self): return self._finish_code + @property + def notes(self): + return self._notes + def __repr__(self): return "".format(**self.__dict__) @@ -63,7 +69,9 @@ def _parse_element(cls, element, ns): started_at = parse_datetime(element.get('startedAt', None)) completed_at = parse_datetime(element.get('completedAt', None)) finish_code = element.get('finishCode', -1) - return cls(id_, type_, progress, created_at, started_at, completed_at, finish_code) + notes = [note.text for note in + element.findall('.//t:notes', namespaces=ns)] or None + return cls(id_, type_, progress, created_at, started_at, completed_at, finish_code, notes) class BackgroundJobItem(object): diff --git a/test/assets/job_get_by_id.xml b/test/assets/job_get_by_id.xml new file mode 100644 index 000000000..b142dfe2f --- /dev/null +++ b/test/assets/job_get_by_id.xml @@ -0,0 +1,14 @@ + + + + + Job detail notes + + + More detail + + + diff --git a/test/test_job.py b/test/test_job.py index ee80450ca..08b98b815 100644 --- a/test/test_job.py +++ b/test/test_job.py @@ -4,10 +4,12 @@ import requests_mock import tableauserverclient as TSC from tableauserverclient.datetime_helpers import utc +from ._utils import read_xml_asset TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets') -GET_XML = os.path.join(TEST_ASSET_DIR, 'job_get.xml') +GET_XML = 'job_get.xml' +GET_BY_ID_XML = 'job_get_by_id.xml' class JobTests(unittest.TestCase): @@ -22,8 +24,7 @@ def setUp(self): self.baseurl = self.server.jobs.baseurl def test_get(self): - with open(GET_XML, 'rb') as f: - response_xml = f.read().decode('utf-8') + response_xml = read_xml_asset(GET_XML) with requests_mock.mock() as m: m.get(self.baseurl, text=response_xml) all_jobs, pagination_item = self.server.jobs.get() @@ -41,6 +42,19 @@ def test_get(self): self.assertEqual(started_at, job.started_at) self.assertEqual(ended_at, job.ended_at) + def test_get_by_id(self): + response_xml = read_xml_asset(GET_BY_ID_XML) + job_id = '2eef4225-aa0c-41c4-8662-a76d89ed7336' + with requests_mock.mock() as m: + m.get('{0}/{1}'.format(self.baseurl, job_id), text=response_xml) + job = self.server.jobs.get_by_id(job_id) + + created_at = datetime(2020, 5, 13, 20, 23, 45, tzinfo=utc) + updated_at = datetime(2020, 5, 13, 20, 25, 18, tzinfo=utc) + ended_at = datetime(2020, 5, 13, 20, 25, 18, tzinfo=utc) + self.assertEqual(job_id, job.id) + self.assertListEqual(job.notes, ['Job detail notes']) + def test_get_before_signin(self): self.server._auth_token = None self.assertRaises(TSC.NotSignedInError, self.server.jobs.get) diff --git a/test/test_task.py b/test/test_task.py index cf7879305..789f97187 100644 --- a/test/test_task.py +++ b/test/test_task.py @@ -105,7 +105,7 @@ def test_get_materializeviews_tasks(self): self.assertEqual(parse_datetime('2019-12-09T22:30:00Z'), task.schedule_item.next_run_at) self.assertEqual(parse_datetime('2019-12-09T20:45:04Z'), task.last_run_at) - def test_delete(self): + def test_delete_data_acceleration(self): with requests_mock.mock() as m: m.delete('{}/{}/{}'.format( self.server.tasks.baseurl, TaskItem.Type.DataAcceleration, diff --git a/test/test_workbook.py b/test/test_workbook.py index 48b47e60f..4ad18d779 100644 --- a/test/test_workbook.py +++ b/test/test_workbook.py @@ -2,6 +2,7 @@ #%% import unittest import os +import re import requests_mock import tableauserverclient as TSC import xml.etree.ElementTree as ET @@ -461,8 +462,8 @@ def test_publish_with_hidden_view(self): hidden_views=['GDP per capita']) request_body = m._adapter.request_history[0]._request.body - self.assertIn( - b'', request_body) + self.assertTrue(re.search(rb'<\/views>', request_body)) + self.assertTrue(re.search(rb'<\/views>', request_body)) def test_publish_async(self): self.server.version = '3.0'