Skip to content

Commit

Permalink
Add custom session injection, Fix bug for http_options (#1119)
Browse files Browse the repository at this point in the history
* ssl-verify is an option, not a header
* Allow injection of session_factory to allow use of a custom session
* show server info (#1118)
* Fix bug in exposing ExcelRequestOptions and test (#1123)
* Fix a few pylint errors (#1124)

Co-authored-by: Marwan Baghdad <[email protected]>
Co-authored-by: jorwoods <[email protected]>
Co-authored-by: Brian Cantoni <[email protected]>
  • Loading branch information
4 people authored Sep 28, 2022
1 parent f653e15 commit ef9e7fd
Show file tree
Hide file tree
Showing 17 changed files with 174 additions and 69 deletions.
10 changes: 7 additions & 3 deletions contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,22 @@ pytest
pip install .
```

### Debugging Tools
See what your outgoing requests look like: https://requestbin.net/ (unaffiliated link not under our control)


### 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
# this will run the formatter without making changes
black --line-length 120 tableauserverclient test samples --check
black . --check

# this will format the directory and code for you
black --line-length 120 tableauserverclient test samples
black .

# this will run type checking
pip install mypy
mypy --show-error-codes --disable-error-code misc --disable-error-code import tableauserverclient test
mypy tableauserverclient test samples
```
2 changes: 1 addition & 1 deletion samples/create_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def main():
logging.basicConfig(level=logging_level)

tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site)
server = TSC.Server(args.server, use_server_version=True)
server = TSC.Server(args.server, use_server_version=True, http_options={"verify": False})
with server.auth.sign_in(tableau_auth):
# this code shows 3 different error codes that mean "resource is already in collection"
# 409009: group already exists on server
Expand Down
6 changes: 3 additions & 3 deletions samples/initialize_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ def main():

# Create the site if it doesn't exist
if existing_site is None:
print("Site not found: {0} Creating it...").format(args.site_id)
print("Site not found: {0} Creating it...".format(args.site_id))
new_site = TSC.SiteItem(
name=args.site_id,
content_url=args.site_id.replace(" ", ""),
admin_mode=TSC.SiteItem.AdminMode.ContentAndUsers,
)
server.sites.create(new_site)
else:
print("Site {0} exists. Moving on...").format(args.site_id)
print("Site {0} exists. Moving on...".format(args.site_id))

################################################################################
# Step 3: Sign-in to our target site
Expand All @@ -87,7 +87,7 @@ def main():

# Create our project if it doesn't exist
if project is None:
print("Project not found: {0} Creating it...").format(args.project)
print("Project not found: {0} Creating it...".format(args.project))
new_project = TSC.ProjectItem(name=args.project)
project = server_upload.projects.create(new_project)

Expand Down
1 change: 1 addition & 0 deletions tableauserverclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
)
from .server import (
CSVRequestOptions,
ExcelRequestOptions,
ImageRequestOptions,
PDFRequestOptions,
RequestOptions,
Expand Down
4 changes: 0 additions & 4 deletions tableauserverclient/models/flow_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,6 @@ def description(self, value: str) -> None:
def project_name(self) -> Optional[str]:
return self._project_name

@property
def flow_type(self): # What is this? It doesn't seem to get set anywhere.
return self._flow_type

@property
def updated_at(self) -> Optional["datetime.datetime"]:
return self._updated_at
Expand Down
2 changes: 1 addition & 1 deletion tableauserverclient/models/permissions_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def from_response(cls, resp, ns=None) -> List["PermissionsRule"]:
mode = capability_xml.get("mode")

if name is None or mode is None:
logger.error("Capability was not valid: ", capability_xml)
logger.error("Capability was not valid: {}".format(capability_xml))
raise UnpopulatedPropertyError()
else:
capability_dict[name] = mode
Expand Down
5 changes: 3 additions & 2 deletions tableauserverclient/models/revision_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ def user_name(self) -> Optional[str]:

def __repr__(self):
return (
"<RevisionItem# revisionNumber={_revision_number} " "current={_current} deleted={_deleted} user={_user_id}>"
).format(**self.__dict__)
"<RevisionItem# revisionNumber={_revision_number} "
"current={_current} deleted={_deleted} user={_user_id}>".format(**self.__dict__)
)

@classmethod
def from_response(cls, resp: bytes, ns, resource_item) -> List["RevisionItem"]:
Expand Down
9 changes: 8 additions & 1 deletion tableauserverclient/models/server_info_item.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import warnings
import xml

from defusedxml.ElementTree import fromstring


Expand Down Expand Up @@ -32,7 +35,11 @@ def rest_api_version(self):

@classmethod
def from_response(cls, resp, ns):
parsed_response = fromstring(resp)
try:
parsed_response = fromstring(resp)
except xml.etree.ElementTree.ParseError as error:
warnings.warn("Unexpected response for ServerInfo: {}".format(resp))
return cls("Unknown", "Unknown", "Unknown")
product_version_tag = parsed_response.find(".//t:productVersion", namespaces=ns)
rest_api_version_tag = parsed_response.find(".//t:restApiVersion", namespaces=ns)

Expand Down
1 change: 0 additions & 1 deletion tableauserverclient/models/site_item.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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,
Expand Down
2 changes: 1 addition & 1 deletion tableauserverclient/models/tableau_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def credentials(self):
+"This method returns values to set as an attribute on the credentials element of the request"

def __repr__(self):
display = "All Credentials types must have a debug display that does not print secrets"
return "All Credentials types must have a debug display that does not print secrets"


def deprecate_site_attribute():
Expand Down
1 change: 1 addition & 0 deletions tableauserverclient/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .request_factory import RequestFactory
from .request_options import (
CSVRequestOptions,
ExcelRequestOptions,
ImageRequestOptions,
PDFRequestOptions,
RequestOptions,
Expand Down
2 changes: 1 addition & 1 deletion tableauserverclient/server/endpoint/databases_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def update_table_default_permissions(self, item):

@api(version="3.5")
def delete_table_default_permissions(self, item):
self._default_permissions.delete_default_permissions(item, Resource.Table)
self._default_permissions.delete_default_permission(item, Resource.Table)

@api(version="3.5")
def populate_dqw(self, item):
Expand Down
34 changes: 13 additions & 21 deletions tableauserverclient/server/endpoint/endpoint.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import requests
import logging
from distutils.version import LooseVersion as Version
from packaging.version import Version
from functools import wraps
from xml.etree.ElementTree import ParseError
from typing import Any, Callable, Dict, Optional, TYPE_CHECKING
Expand All @@ -11,9 +11,12 @@
NonXMLResponseError,
EndpointUnavailableError,
)
from .. import endpoint
from ..query import QuerySet
from ... import helpers
from ..._version import get_versions

__TSC_VERSION__ = get_versions()["version"]
del get_versions

logger = logging.getLogger("tableau.endpoint")

Expand All @@ -22,34 +25,25 @@
XML_CONTENT_TYPE = "text/xml"
JSON_CONTENT_TYPE = "application/json"

USERAGENT_HEADER = "User-Agent"

if TYPE_CHECKING:
from ..server import Server
from requests import Response


_version_header: Optional[str] = None


class Endpoint(object):
def __init__(self, parent_srv: "Server"):
global _version_header
self.parent_srv = parent_srv

@staticmethod
def _make_common_headers(auth_token, content_type):
global _version_header

if not _version_header:
from ..server import __TSC_VERSION__

_version_header = __TSC_VERSION__

headers = {}
if auth_token is not None:
headers["x-tableau-auth"] = auth_token
if content_type is not None:
headers["content-type"] = content_type
headers["User-Agent"] = "Tableau Server Client/{}".format(_version_header)
headers["User-Agent"] = "Tableau Server Client/{}".format(__TSC_VERSION__)
return headers

def _make_request(
Expand All @@ -62,9 +56,9 @@ def _make_request(
parameters: Optional[Dict[str, Any]] = None,
) -> "Response":
parameters = parameters or {}
parameters.update(self.parent_srv.http_options)
if "headers" not in parameters:
parameters["headers"] = {}
parameters.update(self.parent_srv.http_options)
parameters["headers"].update(Endpoint._make_common_headers(auth_token, content_type))

if content is not None:
Expand All @@ -89,14 +83,12 @@ def _check_status(self, server_response, url: str = None):
if server_response.status_code >= 500:
raise InternalServerError(server_response, url)
elif server_response.status_code not in Success_codes:
# todo: is an error reliably of content-type application/xml?
try:
raise ServerResponseError.from_response(server_response.content, self.parent_srv.namespace, url)
except ParseError:
# This will happen if we get a non-success HTTP code that
# doesn't return an xml error object (like metadata endpoints or 503 pages)
# we convert this to a better exception and pass through the raw
# response body
# This will happen if we get a non-success HTTP code that doesn't return an xml error object
# e.g metadata endpoints, 503 pages, totally different servers
# we convert this to a better exception and pass through the raw response body
raise NonXMLResponseError(server_response.content)
except Exception:
# anything else re-raise here
Expand Down Expand Up @@ -194,7 +186,7 @@ def api(version):
def _decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
self.parent_srv.assert_at_least_version(version, "endpoint")
self.parent_srv.assert_at_least_version(version, self.__class__.__name__)
return func(self, *args, **kwargs)

return wrapper
Expand Down
21 changes: 17 additions & 4 deletions tableauserverclient/server/endpoint/server_info_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@


class ServerInfo(Endpoint):
def __init__(self, server):
self.parent_srv = server
self._info = None

@property
def serverInfo(self):
if not self._info:
self.get()
return self._info

def __repr__(self):
return "<Endpoint {}>".format(self.serverInfo)

@property
def baseurl(self):
return "{0}/serverInfo".format(self.parent_srv.baseurl)
Expand All @@ -23,10 +36,10 @@ def get(self):
server_response = self.get_unauthenticated_request(self.baseurl)
except ServerResponseError as e:
if e.code == "404003":
raise ServerInfoEndpointNotFoundError
raise ServerInfoEndpointNotFoundError(e)
if e.code == "404001":
raise EndpointUnavailableError
raise EndpointUnavailableError(e)
raise e

server_info = ServerInfoItem.from_response(server_response.content, self.parent_srv.namespace)
return server_info
self._info = ServerInfoItem.from_response(server_response.content, self.parent_srv.namespace)
return self._info
Loading

1 comment on commit ef9e7fd

@sibbiii
Copy link

@sibbiii sibbiii commented on ef9e7fd Oct 5, 2022

Choose a reason for hiding this comment

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

Hi,

@MrwanBaghdad
The print("==================") in tableauserverclient/server/server.py should not be there i guess.

Thanks,
Sebastian

Please sign in to comment.