Skip to content

Commit

Permalink
Merge branch 'jac/export-language' of github.com:tableau/server-clien…
Browse files Browse the repository at this point in the history
…t-python into jac/export-language
  • Loading branch information
jacalata committed Oct 10, 2024
2 parents 871495a + 6317765 commit 4f15237
Show file tree
Hide file tree
Showing 14 changed files with 280 additions and 22 deletions.
5 changes: 5 additions & 0 deletions samples/explore_datasource.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def main():
if args.publish:
if default_project is not None:
new_datasource = TSC.DatasourceItem(default_project.id)
new_datasource.description = "Published with a description"
new_datasource = server.datasources.publish(
new_datasource, args.publish, TSC.Server.PublishMode.Overwrite
)
Expand All @@ -72,6 +73,10 @@ def main():
print(f"\nConnections for {sample_datasource.name}: ")
print([f"{connection.id}({connection.datasource_name})" for connection in sample_datasource.connections])

# Demonstrate that description is editable
sample_datasource.description = "Description updated by TSC"
server.datasources.update(sample_datasource)

# Add some tags to the datasource
original_tag_set = set(sample_datasource.tags)
sample_datasource.tags.update("a", "b", "c", "d")
Expand Down
2 changes: 2 additions & 0 deletions tableauserverclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
PDFRequestOptions,
RequestOptions,
MissingRequiredFieldError,
FailedSignInError,
NotSignedInError,
ServerResponseError,
Filter,
Expand All @@ -82,6 +83,7 @@
"DEFAULT_NAMESPACE",
"DQWItem",
"ExcelRequestOptions",
"FailedSignInError",
"FavoriteItem",
"FileuploadItem",
"Filter",
Expand Down
8 changes: 5 additions & 3 deletions tableauserverclient/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@

DELAY_SLEEP_SECONDS = 0.1

# The maximum size of a file that can be published in a single request is 64MB
FILESIZE_LIMIT_MB = 64


class Config:
# The maximum size of a file that can be published in a single request is 64MB
@property
def FILESIZE_LIMIT_MB(self):
return min(int(os.getenv("TSC_FILESIZE_LIMIT_MB", 64)), 64)

# For when a datasource is over 64MB, break it into 5MB(standard chunk size) chunks
@property
def CHUNK_SIZE_MB(self):
Expand Down
8 changes: 3 additions & 5 deletions tableauserverclient/models/server_info_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,11 @@ def from_response(cls, resp, ns):
try:
parsed_response = fromstring(resp)
except xml.etree.ElementTree.ParseError as error:
logger.info(f"Unexpected response for ServerInfo: {resp}")
logger.info(error)
logger.exception(f"Unexpected response for ServerInfo: {resp}")
return cls("Unknown", "Unknown", "Unknown")
except Exception as error:
logger.info(f"Unexpected response for ServerInfo: {resp}")
logger.info(error)
return cls("Unknown", "Unknown", "Unknown")
logger.exception(f"Unexpected response for ServerInfo: {resp}")
raise error

product_version_tag = parsed_response.find(".//t:productVersion", namespaces=ns)
rest_api_version_tag = parsed_response.find(".//t:restApiVersion", namespaces=ns)
Expand Down
110 changes: 110 additions & 0 deletions tableauserverclient/models/tableau_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,43 @@ def deprecate_site_attribute():

# The traditional auth type: username/password
class TableauAuth(Credentials):
"""
The TableauAuth class defines the information you can set in a sign-in
request. The class members correspond to the attributes of a server request
or response payload. To use this class, create a new instance, supplying
user name, password, and site information if necessary, and pass the
request object to the Auth.sign_in method.
Parameters
----------
username : str
The user name for the sign-in request.
password : str
The password for the sign-in request.
site_id : str, optional
This corresponds to the contentUrl attribute in the Tableau REST API.
The site_id is the portion of the URL that follows the /site/ in the
URL. For example, "MarketingTeam" is the site_id in the following URL
MyServer/#/site/MarketingTeam/projects. To specify the default site on
Tableau Server, you can use an empty string '' (single quotes, no
space). For Tableau Cloud, you must provide a value for the site_id.
user_id_to_impersonate : str, optional
Specifies the id (not the name) of the user to sign in as. This is not
available for Tableau Online.
Examples
--------
>>> import tableauserverclient as TSC
>>> tableau_auth = TSC.TableauAuth('USERNAME', 'PASSWORD', site_id='CONTENTURL')
>>> server = TSC.Server('https://SERVER_URL', use_server_version=True)
>>> server.auth.sign_in(tableau_auth)
"""

def __init__(
self, username: str, password: str, site_id: Optional[str] = None, user_id_to_impersonate: Optional[str] = None
) -> None:
Expand All @@ -55,6 +92,43 @@ def __repr__(self):

# A Tableau-generated Personal Access Token
class PersonalAccessTokenAuth(Credentials):
"""
The PersonalAccessTokenAuth class defines the information you can set in a sign-in
request. The class members correspond to the attributes of a server request
or response payload. To use this class, create a new instance, supplying
token name, token secret, and site information if necessary, and pass the
request object to the Auth.sign_in method.
Parameters
----------
token_name : str
The name of the personal access token.
personal_access_token : str
The personal access token secret for the sign in request.
site_id : str, optional
This corresponds to the contentUrl attribute in the Tableau REST API.
The site_id is the portion of the URL that follows the /site/ in the
URL. For example, "MarketingTeam" is the site_id in the following URL
MyServer/#/site/MarketingTeam/projects. To specify the default site on
Tableau Server, you can use an empty string '' (single quotes, no
space). For Tableau Cloud, you must provide a value for the site_id.
user_id_to_impersonate : str, optional
Specifies the id (not the name) of the user to sign in as. This is not
available for Tableau Online.
Examples
--------
>>> import tableauserverclient as TSC
>>> tableau_auth = TSC.PersonalAccessTokenAuth("token_name", "token_secret", site_id='CONTENTURL')
>>> server = TSC.Server('https://SERVER_URL', use_server_version=True)
>>> server.auth.sign_in(tableau_auth)
"""

def __init__(
self,
token_name: str,
Expand Down Expand Up @@ -88,6 +162,42 @@ def __repr__(self):

# A standard JWT generated specifically for Tableau
class JWTAuth(Credentials):
"""
The JWTAuth class defines the information you can set in a sign-in
request. The class members correspond to the attributes of a server request
or response payload. To use this class, create a new instance, supplying
an encoded JSON Web Token, and site information if necessary, and pass the
request object to the Auth.sign_in method.
Parameters
----------
token : str
The encoded JSON Web Token.
site_id : str, optional
This corresponds to the contentUrl attribute in the Tableau REST API.
The site_id is the portion of the URL that follows the /site/ in the
URL. For example, "MarketingTeam" is the site_id in the following URL
MyServer/#/site/MarketingTeam/projects. To specify the default site on
Tableau Server, you can use an empty string '' (single quotes, no
space). For Tableau Cloud, you must provide a value for the site_id.
user_id_to_impersonate : str, optional
Specifies the id (not the name) of the user to sign in as. This is not
available for Tableau Online.
Examples
--------
>>> import jwt
>>> import tableauserverclient as TSC
>>> jwt_token = jwt.encode(...)
>>> tableau_auth = TSC.JWTAuth(token, site_id='CONTENTURL')
>>> server = TSC.Server('https://SERVER_URL', use_server_version=True)
>>> server.auth.sign_in(tableau_auth)
"""

def __init__(self, jwt: str, site_id: Optional[str] = None, user_id_to_impersonate: Optional[str] = None) -> None:
if jwt is None:
raise TabError("Must provide a JWT token when using JWT authentication")
Expand Down
3 changes: 2 additions & 1 deletion tableauserverclient/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from tableauserverclient.server.sort import Sort
from tableauserverclient.server.server import Server
from tableauserverclient.server.pager import Pager
from tableauserverclient.server.endpoint.exceptions import NotSignedInError
from tableauserverclient.server.endpoint.exceptions import FailedSignInError, NotSignedInError

from tableauserverclient.server.endpoint import (
Auth,
Expand Down Expand Up @@ -57,6 +57,7 @@
"Sort",
"Server",
"Pager",
"FailedSignInError",
"NotSignedInError",
"Auth",
"CustomViews",
Expand Down
57 changes: 57 additions & 0 deletions tableauserverclient/server/endpoint/auth_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,30 @@ def sign_in(self, auth_req: "Credentials") -> contextmgr:
optionally a user_id to impersonate.
Creates a context manager that will sign out of the server upon exit.
Parameters
----------
auth_req : Credentials
The credentials object to use for signing in. Can be a TableauAuth,
PersonalAccessTokenAuth, or JWTAuth object.
Returns
-------
contextmgr
A context manager that will sign out of the server upon exit.
Examples
--------
>>> import tableauserverclient as TSC
>>> # create an auth object
>>> tableau_auth = TSC.TableauAuth('USERNAME', 'PASSWORD')
>>> # create an instance for your server
>>> server = TSC.Server('https://SERVER_URL')
>>> # call the sign-in method with the auth object
>>> server.auth.sign_in(tableau_auth)
"""
url = f"{self.baseurl}/signin"
signin_req = RequestFactory.Auth.signin_req(auth_req)
Expand Down Expand Up @@ -70,14 +94,17 @@ def sign_in(self, auth_req: "Credentials") -> contextmgr:
# The distinct methods are mostly useful for explicitly showing api version support for each auth type
@api(version="3.6")
def sign_in_with_personal_access_token(self, auth_req: "Credentials") -> contextmgr:
"""Passthrough to sign_in method"""
return self.sign_in(auth_req)

@api(version="3.17")
def sign_in_with_json_web_token(self, auth_req: "Credentials") -> contextmgr:
"""Passthrough to sign_in method"""
return self.sign_in(auth_req)

@api(version="2.0")
def sign_out(self) -> None:
"""Sign out of current session."""
url = f"{self.baseurl}/signout"
# If there are no auth tokens you're already signed out. No-op
if not self.parent_srv.is_signed_in():
Expand All @@ -88,6 +115,33 @@ def sign_out(self) -> None:

@api(version="2.6")
def switch_site(self, site_item: "SiteItem") -> contextmgr:
"""
Switch to a different site on the server. This will sign out of the
current site and sign in to the new site. If used as a context manager,
will sign out of the new site upon exit.
Parameters
----------
site_item : SiteItem
The site to switch to.
Returns
-------
contextmgr
A context manager that will sign out of the new site upon exit.
Examples
--------
>>> import tableauserverclient as TSC
>>> # Find the site you want to switch to
>>> new_site = server.sites.get_by_id("9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d")
>>> # switch to the new site
>>> with server.auth.switch_site(new_site):
>>> # do something on the new site
>>> pass
"""
url = f"{self.baseurl}/switchSite"
switch_req = RequestFactory.Auth.switch_req(site_item.content_url)
try:
Expand All @@ -109,6 +163,9 @@ def switch_site(self, site_item: "SiteItem") -> contextmgr:

@api(version="3.10")
def revoke_all_server_admin_tokens(self) -> None:
"""
Revokes all personal access tokens for all server admins on the server.
"""
url = f"{self.baseurl}/revokeAllServerAdminTokens"
self.post_request(url, "")
logger.info("Revoked all tokens for all server admins")
4 changes: 2 additions & 2 deletions tableauserverclient/server/endpoint/custom_views_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pathlib import Path
from typing import Optional, Union

from tableauserverclient.config import BYTES_PER_MB, FILESIZE_LIMIT_MB
from tableauserverclient.config import BYTES_PER_MB, config
from tableauserverclient.filesys_helpers import get_file_object_size
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
Expand Down Expand Up @@ -144,7 +144,7 @@ def publish(self, view_item: CustomViewItem, file: PathOrFileR) -> Optional[Cust
else:
raise ValueError("File path or file object required for publishing custom view.")

if size >= FILESIZE_LIMIT_MB * BYTES_PER_MB:
if size >= config.FILESIZE_LIMIT_MB * BYTES_PER_MB:
upload_session_id = self.parent_srv.fileuploads.upload(file)
url = f"{url}?uploadSessionId={upload_session_id}"
xml_request, content_type = RequestFactory.CustomView.publish_req_chunked(view_item)
Expand Down
6 changes: 3 additions & 3 deletions tableauserverclient/server/endpoint/datasources_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin

from tableauserverclient.config import ALLOWED_FILE_EXTENSIONS, FILESIZE_LIMIT_MB, BYTES_PER_MB, config
from tableauserverclient.config import ALLOWED_FILE_EXTENSIONS, BYTES_PER_MB, config
from tableauserverclient.filesys_helpers import (
make_download_path,
get_file_type,
Expand Down Expand Up @@ -268,10 +268,10 @@ def publish(
url += "&{}=true".format("asJob")

# Determine if chunking is required (64MB is the limit for single upload method)
if file_size >= FILESIZE_LIMIT_MB * BYTES_PER_MB:
if file_size >= config.FILESIZE_LIMIT_MB * BYTES_PER_MB:
logger.info(
"Publishing {} to server with chunking method (datasource over {}MB, chunk size {}MB)".format(
filename, FILESIZE_LIMIT_MB, config.CHUNK_SIZE_MB
filename, config.FILESIZE_LIMIT_MB, config.CHUNK_SIZE_MB
)
)
upload_session_id = self.parent_srv.fileuploads.upload(file)
Expand Down
3 changes: 2 additions & 1 deletion tableauserverclient/server/endpoint/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from tableauserverclient.server.request_options import RequestOptions

from tableauserverclient.server.endpoint.exceptions import (
FailedSignInError,
ServerResponseError,
InternalServerError,
NonXMLResponseError,
Expand Down Expand Up @@ -160,7 +161,7 @@ def _check_status(self, server_response: "Response", url: Optional[str] = None):
try:
if server_response.status_code == 401:
# TODO: catch this in server.py and attempt to sign in again, in case it's a session expiry
raise NotSignedInError(server_response.content, url)
raise FailedSignInError.from_response(server_response.content, self.parent_srv.namespace, url)

raise ServerResponseError.from_response(server_response.content, self.parent_srv.namespace, url)
except ParseError:
Expand Down
Loading

0 comments on commit 4f15237

Please sign in to comment.