Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions samples/explore_site.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
####
# This script demonstrates how to use the Tableau Server Client
# to interact with sites.
####

import argparse
import logging
import os.path
import sys

import tableauserverclient as TSC


def main():

parser = argparse.ArgumentParser(description="Explore site updates by the Server API.")
# Common options; please keep those in sync across all samples
parser.add_argument("--server", "-s", required=True, help="server address")
parser.add_argument("--site", "-S", help="site name")
parser.add_argument(
"--token-name", "-p", required=True, help="name of the personal access token used to sign into the server"
)
parser.add_argument(
"--token-value", "-v", required=True, help="value of the personal access token used to sign into the server"
)
parser.add_argument(
"--logging-level",
"-l",
choices=["debug", "info", "error"],
default="error",
help="desired logging level (set to error by default)",
)

parser.add_argument("--delete")
parser.add_argument("--create")
parser.add_argument("--url")
parser.add_argument("--new_site_name")
parser.add_argument("--user_quota")
parser.add_argument("--storage_quota")
parser.add_argument("--status")

args = parser.parse_args()

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

# SIGN IN
tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site)
server = TSC.Server(args.server, use_server_version=True)
new_site = None
with server.auth.sign_in(tableau_auth):
current_site = server.sites.get_by_id(server.site_id)

if args.delete:
print("You can only delete the site you are currently in")
print("Delete site `{}`?".format(current_site.name))
# server.sites.delete(server.site_id)

elif args.create:
new_site = TSC.SiteItem(args.create, args.url or args.create)
site_item = server.sites.create(new_site)
print(site_item)
# to do anything further with the site, you need to log into it
# if a PAT is required, that means going to the UI to create one

else:
new_site = current_site
print(current_site, "current user quota:", current_site.user_quota)
print("Remember, you can only update the site you are currently in")
if args.url:
new_site.content_url = args.url
if args.user_quota:
new_site.user_quota = args.user_quota
try:
updated_site = server.sites.update(new_site)
print(updated_site, "new user quota:", updated_site.user_quota)
except TSC.ServerResponseError as e:
print(e)


if __name__ == "__main__":
main()
5 changes: 4 additions & 1 deletion samples/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ def main():
count = 0
for resource in TSC.Pager(endpoint.get, options):
count = count + 1
print(resource.id, resource.name)
# endpoint.populate_connections(resource)
print(resource.name[:18], " ") # , resource._connections())
if count > 100:
break
print("Total: {}".format(count))


Expand Down
51 changes: 26 additions & 25 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,44 @@
from distutils.core import setup

from os import path

this_directory = path.abspath(path.dirname(__file__))
with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f:
with open(path.join(this_directory, "README.md"), encoding="utf-8") as f:
long_description = f.read()

# Only install pytest and runner when test command is run
# This makes work easier for offline installs or low bandwidth machines
needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv)
pytest_runner = ['pytest-runner'] if needs_pytest else []
test_requirements = ['black', 'mock', 'pytest', 'requests-mock>=1.0,<2.0', 'mypy>=0.920']
needs_pytest = {"pytest", "test", "ptr"}.intersection(sys.argv)
pytest_runner = ["pytest-runner"] if needs_pytest else []
test_requirements = ["black", "mock", "pytest", "requests-mock>=1.0,<2.0", "mypy>=0.920"]

setup(
name='tableauserverclient',
name="tableauserverclient",
version=versioneer.get_version(),
cmdclass=versioneer.get_cmdclass(),
author='Tableau',
author_email='github@tableau.com',
url='https://github.com/tableau/server-client-python',
package_data={'tableauserverclient':['py.typed']},
packages=['tableauserverclient',
'tableauserverclient.helpers',
'tableauserverclient.models',
'tableauserverclient.server',
'tableauserverclient.server.endpoint'],
license='MIT',
description='A Python module for working with the Tableau Server REST API.',
author="Tableau",
author_email="github@tableau.com",
url="https://github.com/tableau/server-client-python",
package_data={"tableauserverclient": ["py.typed"]},
packages=[
"tableauserverclient",
"tableauserverclient.helpers",
"tableauserverclient.models",
"tableauserverclient.server",
"tableauserverclient.server.endpoint",
],
license="MIT",
description="A Python module for working with the Tableau Server REST API.",
long_description=long_description,
long_description_content_type='text/markdown',
test_suite='test',
long_description_content_type="text/markdown",
test_suite="test",
setup_requires=pytest_runner,
install_requires=[
'defusedxml>=0.7.1',
'requests>=2.11,<3.0',
"defusedxml>=0.7.1",
"requests>=2.11,<3.0",
],
python_requires='>3.7.0',
python_requires=">3.7.0",
tests_require=test_requirements,
extras_require={
'test': test_requirements
},
zip_safe=False
extras_require={"test": test_requirements},
zip_safe=False,
)
4 changes: 2 additions & 2 deletions tableauserverclient/datetime_helpers.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import datetime

# This code below is from the python documentation for
# tzinfo: https://docs.python.org/2.3/lib/datetime-tzinfo.html

ZERO = datetime.timedelta(0)
HOUR = datetime.timedelta(hours=1)


# This class is a concrete implementation of the abstract base class tzinfo
# docs: https://docs.python.org/2.3/lib/datetime-tzinfo.html
class UTC(datetime.tzinfo):
"""UTC"""

Expand Down
32 changes: 27 additions & 5 deletions tableauserverclient/models/site_item.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import warnings
import xml.etree.ElementTree as ET

from distutils.version import Version
from defusedxml.ElementTree import fromstring

from .property_decorators import (
property_is_enum,
property_is_boolean,
Expand All @@ -14,7 +14,10 @@

VALID_CONTENT_URL_RE = r"^[a-zA-Z0-9_\-]*$"

from typing import List, Optional, Union
from typing import List, Optional, Union, TYPE_CHECKING

if TYPE_CHECKING:
from tableauserverclient.server import Server


class SiteItem(object):
Expand All @@ -23,6 +26,19 @@ class SiteItem(object):
_tier_explorer_capacity: Optional[int] = None
_tier_viewer_capacity: Optional[int] = None

def __str__(self):
return (
"<"
+ __name__
+ ": "
+ (self.name or "unnamed")
+ ", "
+ (self.id or "unknown-id")
+ ", "
+ (self.state or "unknown-state")
+ ">"
)

class AdminMode:
ContentAndUsers: str = "ContentAndUsers"
ContentOnly: str = "ContentOnly"
Expand Down Expand Up @@ -261,18 +277,24 @@ def cataloging_enabled(self) -> bool:
def cataloging_enabled(self, value: bool):
self._cataloging_enabled = value

def is_default(self) -> bool:
return self.name.lower() == "default"

@staticmethod
def use_new_flow_settings(parent_srv: "Server") -> bool:
return parent_srv is not None and parent_srv.check_at_least_version("3.10")

@property
def flows_enabled(self) -> bool:
return self._flows_enabled

@flows_enabled.setter
@property_is_boolean
def flows_enabled(self, value: bool) -> None:
# Flows Enabled' is not a supported site setting in API Version [3.17].
# In Version 3.10+ use the more granular settings 'Editing Flows Enabled' and/or 'Scheduling Flows Enabled'
self._flows_enabled = value

def is_default(self) -> bool:
return self.name.lower() == "default"

@property
def editing_flows_enabled(self) -> bool:
return self._editing_flows_enabled
Expand Down
10 changes: 5 additions & 5 deletions tableauserverclient/server/endpoint/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def _make_request(
logger.debug("request content: {}".format(helpers.strings.redact_xml(content[:1000])))

server_response = method(url, **parameters)
self._check_status(server_response)
self._check_status(server_response, url)

loggable_response = self.log_response_safely(server_response)
logger.debug("Server response from {0}:\n\t{1}".format(url, loggable_response))
Expand All @@ -73,9 +73,9 @@ def _make_request(

return server_response

def _check_status(self, server_response):
def _check_status(self, server_response, request_url=None):
if server_response.status_code >= 500:
raise InternalServerError(server_response)
raise InternalServerError(server_response, request_url)
elif server_response.status_code not in Success_codes:
# todo: is an error reliably of content-type application/xml?
try:
Expand Down Expand Up @@ -112,7 +112,7 @@ def get_request(self, url, request_object=None, parameters=None):
if request_object is not None:
try:
# Query param delimiters don't need to be encoded for versions before 3.7 (2020.1)
self.parent_srv.assert_at_least_version("3.7")
self.parent_srv.assert_at_least_version("3.7", "Query param encoding")
parameters = parameters or {}
parameters["params"] = request_object.get_query_params()
except EndpointUnavailableError:
Expand Down Expand Up @@ -182,7 +182,7 @@ def api(version):
def _decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
self.parent_srv.assert_at_least_version(version)
self.parent_srv.assert_at_least_version(version, "endpoint")
return func(self, *args, **kwargs)

return wrapper
Expand Down
31 changes: 18 additions & 13 deletions tableauserverclient/server/endpoint/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from defusedxml.ElementTree import fromstring


class ServerResponseError(Exception):
class TableauError(Exception):
pass


class ServerResponseError(TableauError):
def __init__(self, code, summary, detail):
self.code = code
self.summary = summary
Expand All @@ -23,40 +27,41 @@ def from_response(cls, resp, ns):
return error_response


class InternalServerError(Exception):
def __init__(self, server_response):
class InternalServerError(TableauError):
def __init__(self, server_response, request_url):
self.code = server_response.status_code
self.content = server_response.content
self.url = request_url or "server"

def __str__(self):
return "\n\nError status code: {0}\n{1}".format(self.code, self.content)
return "\n\nInternal error {0} at {1}\n{2}".format(self.code, self.url, self.content)


class MissingRequiredFieldError(Exception):
class MissingRequiredFieldError(TableauError):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What's the purpose of this class (and the ones below) ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To be able to return more informative errors

pass


class ServerInfoEndpointNotFoundError(Exception):
class ServerInfoEndpointNotFoundError(TableauError):
pass


class EndpointUnavailableError(Exception):
class EndpointUnavailableError(TableauError):
pass


class ItemTypeNotAllowed(Exception):
class ItemTypeNotAllowed(TableauError):
pass


class NonXMLResponseError(Exception):
class NonXMLResponseError(TableauError):
pass


class InvalidGraphQLQuery(Exception):
class InvalidGraphQLQuery(TableauError):
pass


class GraphQLError(Exception):
class GraphQLError(TableauError):
def __init__(self, error_payload):
self.error = error_payload

Expand All @@ -66,7 +71,7 @@ def __str__(self):
return pformat(self.error)


class JobFailedException(Exception):
class JobFailedException(TableauError):
def __init__(self, job):
self.notes = job.notes
self.job = job
Expand All @@ -79,7 +84,7 @@ class JobCancelledException(JobFailedException):
pass


class FlowRunFailedException(Exception):
class FlowRunFailedException(TableauError):
def __init__(self, flow_run):
self.background_job_id = flow_run.background_job_id
self.flow_run = flow_run
Expand Down
2 changes: 1 addition & 1 deletion tableauserverclient/server/endpoint/jobs_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def get(
if isinstance(job_id, RequestOptionsBase):
req_options = job_id

self.parent_srv.assert_at_least_version("3.1")
self.parent_srv.assert_at_least_version("3.1", "Jobs.get_by_id(job_id)")
server_response = self.get_request(self.baseurl, req_options)
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
jobs = BackgroundJobItem.from_response(server_response.content, self.parent_srv.namespace)
Expand Down
Loading