From 7baacec6c19ec5a087f47d9fd8707af3c5f39d23 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 13 Jul 2022 01:08:11 -0700 Subject: [PATCH 1/5] Update sites requests for version changes Breaking change in 3.10: flowsEnabled removed, flowsEditingEnabled and flowsSchedulingEnabled added Added a sample for using the sites calls --- samples/explore_site.py | 83 +++++++++++++++++++ tableauserverclient/models/site_item.py | 14 +++- .../server/endpoint/sites_endpoint.py | 27 ++++-- tableauserverclient/server/request_factory.py | 47 +++++++---- tableauserverclient/server/server.py | 18 ++-- 5 files changed, 160 insertions(+), 29 deletions(-) create mode 100644 samples/explore_site.py diff --git a/samples/explore_site.py b/samples/explore_site.py new file mode 100644 index 000000000..8c4abd9d3 --- /dev/null +++ b/samples/explore_site.py @@ -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() diff --git a/tableauserverclient/models/site_item.py b/tableauserverclient/models/site_item.py index 2d27acabf..4a11bf88d 100644 --- a/tableauserverclient/models/site_item.py +++ b/tableauserverclient/models/site_item.py @@ -23,6 +23,12 @@ 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" @@ -261,6 +267,9 @@ 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" + @property def flows_enabled(self) -> bool: return self._flows_enabled @@ -268,11 +277,10 @@ def flows_enabled(self) -> bool: @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 diff --git a/tableauserverclient/server/endpoint/sites_endpoint.py b/tableauserverclient/server/endpoint/sites_endpoint.py index bdf281fb9..935f4f77a 100644 --- a/tableauserverclient/server/endpoint/sites_endpoint.py +++ b/tableauserverclient/server/endpoint/sites_endpoint.py @@ -22,6 +22,7 @@ def baseurl(self) -> str: @api(version="2.0") def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[SiteItem], PaginationItem]: logger.info("Querying all sites on site") + logger.info("Requires Server Admin permissions") url = self.baseurl server_response = self.get_request(url, req_options) pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) @@ -34,6 +35,10 @@ def get_by_id(self, site_id: str) -> SiteItem: if not site_id: error = "Site ID undefined." raise ValueError(error) + if not site_id == self.parent_srv.site_id: + error = "You can only retrieve the site for which you are currently authenticated." + raise ValueError(error) + logger.info("Querying single site (ID: {0})".format(site_id)) url = "{0}/{1}".format(self.baseurl, site_id) server_response = self.get_request(url) @@ -45,8 +50,10 @@ def get_by_name(self, site_name: str) -> SiteItem: if not site_name: error = "Site Name undefined." raise ValueError(error) + print("Note: You can only work with the site for which you are currently authenticated") logger.info("Querying single site (Name: {0})".format(site_name)) url = "{0}/{1}?key=name".format(self.baseurl, site_name) + print(self.baseurl, url) server_response = self.get_request(url) return SiteItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -56,7 +63,12 @@ def get_by_content_url(self, content_url: str) -> SiteItem: if content_url is None: error = "Content URL undefined." raise ValueError(error) + if not self.parent_srv.baseurl.index(content_url) > 0: + error = "You can only work with the site you are currently authenticated for" + raise ValueError(error) + logger.info("Querying single site (Content URL: {0})".format(content_url)) + logger.debug("Querying other sites requires Server Admin permissions") url = "{0}/{1}?key=contentUrl".format(self.baseurl, content_url) server_response = self.get_request(url) return SiteItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -67,6 +79,10 @@ def update(self, site_item: SiteItem) -> SiteItem: if not site_item.id: error = "Site item missing ID." raise MissingRequiredFieldError(error) + if not site_item.id == self.parent_srv.site_id: + error = "You can only update the site you are currently authenticated for" + raise ValueError(error) + if site_item.admin_mode: if site_item.admin_mode == SiteItem.AdminMode.ContentOnly and site_item.user_quota: error = "You cannot set admin_mode to ContentOnly and also set a user quota" @@ -86,12 +102,11 @@ def delete(self, site_id: str) -> None: error = "Site ID undefined." raise ValueError(error) url = "{0}/{1}".format(self.baseurl, site_id) + if not site_id == self.parent_srv.site_id: + error = "You can only delete the site you are currently authenticated for" + raise ValueError(error) self.delete_request(url) - # 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() + self.parent_srv._clear_auth() logger.info("Deleted single site (ID: {0}) and signed out".format(site_id)) # Create new site @@ -103,7 +118,7 @@ def create(self, site_item: SiteItem) -> SiteItem: raise ValueError(error) url = self.baseurl - create_req = RequestFactory.Site.create_req(site_item) + create_req = RequestFactory.Site.create_req(site_item, self.parent_srv) server_response = self.post_request(url, create_req) new_site = SiteItem.from_response(server_response.content, self.parent_srv.namespace)[0] logger.info("Created new site (ID: {0})".format(new_site.id)) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index fc00ca085..4b5ae6934 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -5,6 +5,7 @@ from requests.packages.urllib3.fields import RequestField from requests.packages.urllib3.filepost import encode_multipart_formdata +import tableauserverclient from tableauserverclient.models.metric_item import MetricItem from ..models import ConnectionItem @@ -556,7 +557,7 @@ def _add_to_req(self, id_: Optional[str], target_type: str, task_type: str = Tas """ if not isinstance(id_, str): - raise ValueError(f"id_ should be a string, reeceived: {type(id_)}") + raise ValueError(f"id_ should be a string, received: {type(id_)}") xml_request = ET.Element("tsRequest") task_element = ET.SubElement(xml_request, "task") task = ET.SubElement(task_element, task_type) @@ -646,15 +647,13 @@ def update_req(self, site_item: "SiteItem"): if site_item.authoring_enabled is not None: site_element.attrib["authoringEnabled"] = str(site_item.authoring_enabled).lower() if site_item.custom_subscription_email_enabled is not None: - site_element.attrib["customSubscriptionEmailEnabled"] = str( - site_item.custom_subscription_email_enabled - ).lower() + site_element.attrib["customSubscriptionEmailEnabled"] = \ + str(site_item.custom_subscription_email_enabled).lower() if site_item.custom_subscription_email is not None: site_element.attrib["customSubscriptionEmail"] = str(site_item.custom_subscription_email).lower() if site_item.custom_subscription_footer_enabled is not None: - site_element.attrib["customSubscriptionFooterEnabled"] = str( - site_item.custom_subscription_footer_enabled - ).lower() + site_element.attrib["customSubscriptionFooterEnabled"] = \ + str(site_item.custom_subscription_footer_enabled).lower() if site_item.custom_subscription_footer is not None: site_element.attrib["customSubscriptionFooter"] = str(site_item.custom_subscription_footer).lower() if site_item.ask_data_mode is not None: @@ -682,7 +681,8 @@ def update_req(self, site_item: "SiteItem"): return ET.tostring(xml_request) - def create_req(self, site_item: "SiteItem"): + # server: the site request model changes based on api version + def create_req(self, site_item: "SiteItem", parent_srv: object=None): xml_request = ET.Element("tsRequest") site_element = ET.SubElement(xml_request, "site") site_element.attrib["name"] = site_item.name @@ -701,12 +701,31 @@ def create_req(self, site_item: "SiteItem"): site_element.attrib["revisionLimit"] = str(site_item.revision_limit) if site_item.data_acceleration_mode is not None: site_element.attrib["dataAccelerationMode"] = str(site_item.data_acceleration_mode).lower() - if site_item.flows_enabled is not None: - site_element.attrib["flowsEnabled"] = str(site_item.flows_enabled).lower() - if site_item.editing_flows_enabled is not None: - site_element.attrib["editingFlowsEnabled"] = str(site_item.editing_flows_enabled).lower() - if site_item.scheduling_flows_enabled is not None: - site_element.attrib["schedulingFlowsEnabled"] = str(site_item.scheduling_flows_enabled).lower() + + flows_edit = str(site_item.editing_flows_enabled).lower() + flows_schedule = str(site_item.scheduling_flows_enabled).lower() + flows_all = str(site_item.flows_enabled).lower() + if parent_srv and parent_srv.check_at_least_version("3.10"): + if site_item.flows_enabled is not None: + flows_edit = flows_edit or flows_all + flows_schedule = flows_schedule or flows_all + import warnings + warnings.warn("FlowsEnabled has been removed and become two options:" + " SchedulingFlowsEnabled and EditingFlowsEnabled") + if site_item.editing_flows_enabled is not None: + site_element.attrib["editingFlowsEnabled"] = flows_edit + if site_item.scheduling_flows_enabled is not None: + site_element.attrib["schedulingFlowsEnabled"] = flows_schedule + + else: # we default to using the older model + if site_item.flows_enabled is not None: + site_element.attrib["flowsEnabled"] = str(site_item.flows_enabled).lower() + if site_item.editing_flows_enabled is not None or site_item.scheduling_flows_enabled is not None: + flows_all = flows_all or flows_edit or flows_schedule + site_element.attrib["flowsEnabled"] = flows_all + import warnings + warnings.warn("In version 3.10 and earlier there is only one option: FlowsEnabled") + if site_item.allow_subscription_attachments is not None: site_element.attrib["allowSubscriptionAttachments"] = str(site_item.allow_subscription_attachments).lower() if site_item.guest_access_enabled is not None: diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index 4522bc272..8691ae0a7 100644 --- a/tableauserverclient/server/server.py +++ b/tableauserverclient/server/server.py @@ -141,15 +141,21 @@ def use_server_version(self): def use_highest_version(self): self.use_server_version() import warnings - warnings.warn("use use_server_version instead", DeprecationWarning) - def assert_at_least_version(self, version): + # sometimes a model changes in newer versions + def check_at_least_version(self, comparison: str): server_version = Version(self.version or "0.0") - minimum_supported = Version(version) - if server_version < minimum_supported: - error = "This endpoint is not available in API version {}. Requires {}".format( - server_version, minimum_supported + compared_version = Version(comparison) + return not server_version < compared_version + + def assert_at_least_version(self, comparison: str): + if self.check_at_least_version(comparison): + return + else: + server_version = Version(self.version or "0.0") + error = "This endpoint (or model) is not available in API version {}. Requires {}".format( + server_version, comparison ) raise EndpointUnavailableError(error) From ff1d5e6c1bcf0059b8290928faadd10979edead6 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 13 Jul 2022 01:09:37 -0700 Subject: [PATCH 2/5] Update exceptions.py create a parent error class to make it easier to catch all tsc errors --- .../server/endpoint/exceptions.py | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/tableauserverclient/server/endpoint/exceptions.py b/tableauserverclient/server/endpoint/exceptions.py index 34de00dd0..fb0f401c4 100644 --- a/tableauserverclient/server/endpoint/exceptions.py +++ b/tableauserverclient/server/endpoint/exceptions.py @@ -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 @@ -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): 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 @@ -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 @@ -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 From 5ba4698e67ccbf097d36e0b52ef7302c5a305fa8 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 13 Jul 2022 01:09:57 -0700 Subject: [PATCH 3/5] minor edits --- samples/list.py | 7 ++++++- tableauserverclient/datetime_helpers.py | 4 ++-- tableauserverclient/server/endpoint/endpoint.py | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/samples/list.py b/samples/list.py index 814c1b9ca..31f599091 100644 --- a/samples/list.py +++ b/samples/list.py @@ -59,9 +59,14 @@ 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)) if __name__ == "__main__": main() + + diff --git a/tableauserverclient/datetime_helpers.py b/tableauserverclient/datetime_helpers.py index 2b1df202c..0d968428d 100644 --- a/tableauserverclient/datetime_helpers.py +++ b/tableauserverclient/datetime_helpers.py @@ -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""" diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index 0acc978d2..cc3b35963 100644 --- a/tableauserverclient/server/endpoint/endpoint.py +++ b/tableauserverclient/server/endpoint/endpoint.py @@ -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)) @@ -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: From 89d9e4706d7d7e49bbe4215165ac3c9c27bc003f Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 13 Jul 2022 01:26:49 -0700 Subject: [PATCH 4/5] update tests and formats --- samples/list.py | 6 +- setup.py | 51 ++-- tableauserverclient/models/site_item.py | 15 +- .../server/endpoint/sites_endpoint.py | 1 + tableauserverclient/server/request_factory.py | 20 +- tableauserverclient/server/server.py | 1 + test/assets/site_get_by_id.xml | 4 +- test/assets/site_get_by_name.xml | 4 +- test/assets/site_update.xml | 4 +- test/test_site.py | 18 +- versioneer.py | 226 ++++++++++-------- 11 files changed, 196 insertions(+), 154 deletions(-) diff --git a/samples/list.py b/samples/list.py index 31f599091..b5cdb38a5 100644 --- a/samples/list.py +++ b/samples/list.py @@ -59,8 +59,8 @@ def main(): count = 0 for resource in TSC.Pager(endpoint.get, options): count = count + 1 - #endpoint.populate_connections(resource) - print(resource.name[:18], " ") #, resource._connections()) + # endpoint.populate_connections(resource) + print(resource.name[:18], " ") # , resource._connections()) if count > 100: break print("Total: {}".format(count)) @@ -68,5 +68,3 @@ def main(): if __name__ == "__main__": main() - - diff --git a/setup.py b/setup.py index 24d35250c..f9b072ec6 100644 --- a/setup.py +++ b/setup.py @@ -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, ) diff --git a/tableauserverclient/models/site_item.py b/tableauserverclient/models/site_item.py index 4a11bf88d..3f6942d1d 100644 --- a/tableauserverclient/models/site_item.py +++ b/tableauserverclient/models/site_item.py @@ -24,10 +24,17 @@ class SiteItem(object): _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") + ">" + return ( + "<" + + __name__ + + ": " + + (self.name or "unnamed") + + ", " + + (self.id or "unknown-id") + + ", " + + (self.state or "unknown-state") + + ">" + ) class AdminMode: ContentAndUsers: str = "ContentAndUsers" diff --git a/tableauserverclient/server/endpoint/sites_endpoint.py b/tableauserverclient/server/endpoint/sites_endpoint.py index 935f4f77a..2797efbf0 100644 --- a/tableauserverclient/server/endpoint/sites_endpoint.py +++ b/tableauserverclient/server/endpoint/sites_endpoint.py @@ -79,6 +79,7 @@ def update(self, site_item: SiteItem) -> SiteItem: if not site_item.id: error = "Site item missing ID." raise MissingRequiredFieldError(error) + print(self.parent_srv.site_id, site_item.id) if not site_item.id == self.parent_srv.site_id: error = "You can only update the site you are currently authenticated for" raise ValueError(error) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 4b5ae6934..01c4b66ac 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -647,13 +647,15 @@ def update_req(self, site_item: "SiteItem"): if site_item.authoring_enabled is not None: site_element.attrib["authoringEnabled"] = str(site_item.authoring_enabled).lower() if site_item.custom_subscription_email_enabled is not None: - site_element.attrib["customSubscriptionEmailEnabled"] = \ - str(site_item.custom_subscription_email_enabled).lower() + site_element.attrib["customSubscriptionEmailEnabled"] = str( + site_item.custom_subscription_email_enabled + ).lower() if site_item.custom_subscription_email is not None: site_element.attrib["customSubscriptionEmail"] = str(site_item.custom_subscription_email).lower() if site_item.custom_subscription_footer_enabled is not None: - site_element.attrib["customSubscriptionFooterEnabled"] = \ - str(site_item.custom_subscription_footer_enabled).lower() + site_element.attrib["customSubscriptionFooterEnabled"] = str( + site_item.custom_subscription_footer_enabled + ).lower() if site_item.custom_subscription_footer is not None: site_element.attrib["customSubscriptionFooter"] = str(site_item.custom_subscription_footer).lower() if site_item.ask_data_mode is not None: @@ -682,7 +684,7 @@ def update_req(self, site_item: "SiteItem"): return ET.tostring(xml_request) # server: the site request model changes based on api version - def create_req(self, site_item: "SiteItem", parent_srv: object=None): + def create_req(self, site_item: "SiteItem", parent_srv: object = None): xml_request = ET.Element("tsRequest") site_element = ET.SubElement(xml_request, "site") site_element.attrib["name"] = site_item.name @@ -710,8 +712,11 @@ def create_req(self, site_item: "SiteItem", parent_srv: object=None): flows_edit = flows_edit or flows_all flows_schedule = flows_schedule or flows_all import warnings - warnings.warn("FlowsEnabled has been removed and become two options:" - " SchedulingFlowsEnabled and EditingFlowsEnabled") + + warnings.warn( + "FlowsEnabled has been removed and become two options:" + " SchedulingFlowsEnabled and EditingFlowsEnabled" + ) if site_item.editing_flows_enabled is not None: site_element.attrib["editingFlowsEnabled"] = flows_edit if site_item.scheduling_flows_enabled is not None: @@ -724,6 +729,7 @@ def create_req(self, site_item: "SiteItem", parent_srv: object=None): flows_all = flows_all or flows_edit or flows_schedule site_element.attrib["flowsEnabled"] = flows_all import warnings + warnings.warn("In version 3.10 and earlier there is only one option: FlowsEnabled") if site_item.allow_subscription_attachments is not None: diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index 8691ae0a7..6c5cdbf5e 100644 --- a/tableauserverclient/server/server.py +++ b/tableauserverclient/server/server.py @@ -141,6 +141,7 @@ def use_server_version(self): def use_highest_version(self): self.use_server_version() import warnings + warnings.warn("use use_server_version instead", DeprecationWarning) # sometimes a model changes in newer versions diff --git a/test/assets/site_get_by_id.xml b/test/assets/site_get_by_id.xml index a47703fb6..a8a1e9a5c 100644 --- a/test/assets/site_get_by_id.xml +++ b/test/assets/site_get_by_id.xml @@ -1,4 +1,4 @@ - - \ No newline at end of file + + diff --git a/test/assets/site_get_by_name.xml b/test/assets/site_get_by_name.xml index 852f9594f..b7ae2b595 100644 --- a/test/assets/site_get_by_name.xml +++ b/test/assets/site_get_by_name.xml @@ -1,4 +1,4 @@ - - \ No newline at end of file + + diff --git a/test/assets/site_update.xml b/test/assets/site_update.xml index dbb166de1..1661a426b 100644 --- a/test/assets/site_update.xml +++ b/test/assets/site_update.xml @@ -1,4 +1,4 @@ - - \ No newline at end of file + + diff --git a/test/test_site.py b/test/test_site.py index 23eb99ddd..b8469e56c 100644 --- a/test/test_site.py +++ b/test/test_site.py @@ -24,6 +24,9 @@ def setUp(self) -> None: self.server._site_id = "0626857c-1def-4503-a7d8-7907c3ff9d9f" self.baseurl = self.server.sites.baseurl + # sites APIs can only be called on the site being logged in to + self.logged_in_site = self.server.site_id + def test_get(self) -> None: with open(GET_XML, "rb") as f: response_xml = f.read().decode("utf-8") @@ -71,10 +74,10 @@ def test_get_by_id(self) -> None: with open(GET_BY_ID_XML, "rb") as f: response_xml = f.read().decode("utf-8") with requests_mock.mock() as m: - m.get(self.baseurl + "/dad65087-b08b-4603-af4e-2887b8aafc67", text=response_xml) - single_site = self.server.sites.get_by_id("dad65087-b08b-4603-af4e-2887b8aafc67") + m.get(self.baseurl + "/" + self.logged_in_site, text=response_xml) + single_site = self.server.sites.get_by_id(self.logged_in_site) - self.assertEqual("dad65087-b08b-4603-af4e-2887b8aafc67", single_site.id) + self.assertEqual(self.logged_in_site, single_site.id) self.assertEqual("Active", single_site.state) self.assertEqual("Default", single_site.name) self.assertEqual("ContentOnly", single_site.admin_mode) @@ -95,7 +98,7 @@ def test_get_by_name(self) -> None: m.get(self.baseurl + "/testsite?key=name", text=response_xml) single_site = self.server.sites.get_by_name("testsite") - self.assertEqual("dad65087-b08b-4603-af4e-2887b8aafc67", single_site.id) + self.assertEqual(self.logged_in_site, single_site.id) self.assertEqual("Active", single_site.state) self.assertEqual("testsite", single_site.name) self.assertEqual("ContentOnly", single_site.admin_mode) @@ -110,7 +113,7 @@ def test_update(self) -> None: with open(UPDATE_XML, "rb") as f: response_xml = f.read().decode("utf-8") with requests_mock.mock() as m: - m.put(self.baseurl + "/6b7179ba-b82b-4f0f-91ed-812074ac5da6", text=response_xml) + m.put(self.baseurl + "/" + self.logged_in_site, text=response_xml) single_site = TSC.SiteItem( name="Tableau", content_url="tableau", @@ -143,10 +146,11 @@ def test_update(self) -> None: tier_explorer_capacity=5, tier_viewer_capacity=5, ) - single_site._id = "6b7179ba-b82b-4f0f-91ed-812074ac5da6" + single_site._id = self.logged_in_site + self.server.sites.parent_srv = self.server single_site = self.server.sites.update(single_site) - self.assertEqual("6b7179ba-b82b-4f0f-91ed-812074ac5da6", single_site.id) + self.assertEqual(self.logged_in_site, single_site.id) self.assertEqual("tableau", single_site.content_url) self.assertEqual("Suspended", single_site.state) self.assertEqual("Tableau", single_site.name) diff --git a/versioneer.py b/versioneer.py index 59211ed6f..86c240e13 100755 --- a/versioneer.py +++ b/versioneer.py @@ -277,6 +277,7 @@ """ from __future__ import print_function + try: import configparser except ImportError: @@ -308,11 +309,13 @@ def get_root(): setup_py = os.path.join(root, "setup.py") versioneer_py = os.path.join(root, "versioneer.py") if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): - err = ("Versioneer was unable to run the project root directory. " - "Versioneer requires setup.py to be executed from " - "its immediate directory (like 'python setup.py COMMAND'), " - "or in a way that lets it use sys.argv[0] to find the root " - "(like 'python path/to/setup.py COMMAND').") + err = ( + "Versioneer was unable to run the project root directory. " + "Versioneer requires setup.py to be executed from " + "its immediate directory (like 'python setup.py COMMAND'), " + "or in a way that lets it use sys.argv[0] to find the root " + "(like 'python path/to/setup.py COMMAND')." + ) raise VersioneerBadRootError(err) try: # Certain runtime workflows (setup.py install/develop in a setuptools @@ -325,8 +328,7 @@ def get_root(): me_dir = os.path.normcase(os.path.splitext(me)[0]) vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) if me_dir != vsr_dir: - print("Warning: build in %s is using versioneer.py from %s" - % (os.path.dirname(me), versioneer_py)) + print("Warning: build in %s is using versioneer.py from %s" % (os.path.dirname(me), versioneer_py)) except NameError: pass return root @@ -348,6 +350,7 @@ def get(parser, name): if parser.has_option("versioneer", name): return parser.get("versioneer", name) return None + cfg = VersioneerConfig() cfg.VCS = VCS cfg.style = get(parser, "style") or "" @@ -372,17 +375,18 @@ class NotThisMethod(Exception): def register_vcs_handler(vcs, method): # decorator """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f + return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) p = None @@ -390,10 +394,9 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, try: dispcmd = str([c] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) + p = subprocess.Popen( + [c] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None) + ) break except EnvironmentError: e = sys.exc_info()[1] @@ -418,7 +421,9 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, return stdout, p.returncode -LONG_VERSION_PY['git'] = ''' +LONG_VERSION_PY[ + "git" +] = ''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build @@ -993,7 +998,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -1002,7 +1007,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) + tags = set([r for r in refs if re.search(r"\d", r)]) if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -1010,19 +1015,26 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] + r = ref[len(tag_prefix) :] if verbose: print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} + return { + "version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": None, + "date": date, + } # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} + return { + "version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": "no suitable tags", + "date": None, + } @register_vcs_handler("git", "pieces_from_vcs") @@ -1037,8 +1049,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -1046,10 +1057,9 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) + describe_out, rc = run_command( + GITS, ["describe", "--tags", "--dirty", "--always", "--long", "--match", "%s*" % tag_prefix], cwd=root + ) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") @@ -1072,17 +1082,16 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] + git_describe = git_describe[: git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) + pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out return pieces # tag @@ -1091,10 +1100,9 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) + pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % (full_tag, tag_prefix) return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] + pieces["closest-tag"] = full_tag[len(tag_prefix) :] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) @@ -1105,13 +1113,11 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) + count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) pieces["distance"] = int(count_out) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], - cwd=root)[0].strip() + date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces @@ -1167,16 +1173,19 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): for i in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} + return { + "version": dirname[len(parentdir_prefix) :], + "full-revisionid": None, + "dirty": False, + "error": None, + "date": None, + } else: rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) + print("Tried directories %s but none started with prefix %s" % (str(rootdirs), parentdir_prefix)) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @@ -1205,11 +1214,9 @@ def versions_from_file(filename): contents = f.read() except EnvironmentError: raise NotThisMethod("unable to read _version.py") - mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) + mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", contents, re.M | re.S) if not mo: - mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) + mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", contents, re.M | re.S) if not mo: raise NotThisMethod("no version_json in _version.py") return json.loads(mo.group(1)) @@ -1218,8 +1225,7 @@ def versions_from_file(filename): def write_to_version_file(filename, versions): """Write the given version number to the given _version.py file.""" os.unlink(filename) - contents = json.dumps(versions, sort_keys=True, - indent=1, separators=(",", ": ")) + contents = json.dumps(versions, sort_keys=True, indent=1, separators=(",", ": ")) with open(filename, "w") as f: f.write(SHORT_VERSION_PY % contents) @@ -1251,8 +1257,7 @@ def render_pep440(pieces): rendered += ".dirty" else: # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) + rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered @@ -1366,11 +1371,13 @@ def render_git_describe_long(pieces): def render(pieces, style): """Render the given version pieces into the requested style.""" if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} + return { + "version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None, + } if not style or style == "default": style = "pep440" # the default @@ -1390,9 +1397,13 @@ def render(pieces, style): else: raise ValueError("unknown style '%s'" % style) - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} + return { + "version": rendered, + "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], + "error": None, + "date": pieces.get("date"), + } class VersioneerBadRootError(Exception): @@ -1415,8 +1426,7 @@ def get_versions(verbose=False): handlers = HANDLERS.get(cfg.VCS) assert handlers, "unrecognized VCS '%s'" % cfg.VCS verbose = verbose or cfg.verbose - assert cfg.versionfile_source is not None, \ - "please set versioneer.versionfile_source" + assert cfg.versionfile_source is not None, "please set versioneer.versionfile_source" assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" versionfile_abs = os.path.join(root, cfg.versionfile_source) @@ -1470,9 +1480,13 @@ def get_versions(verbose=False): if verbose: print("unable to compute version") - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, "error": "unable to compute version", - "date": None} + return { + "version": "0+unknown", + "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", + "date": None, + } def get_version(): @@ -1521,6 +1535,7 @@ def run(self): print(" date: %s" % vers.get("date")) if vers["error"]: print(" error: %s" % vers["error"]) + cmds["version"] = cmd_version # we override "build_py" in both distutils and setuptools @@ -1553,14 +1568,15 @@ def run(self): # now locate _version.py in the new build/ directory and replace # it with an updated value if cfg.versionfile_build: - target_versionfile = os.path.join(self.build_lib, - cfg.versionfile_build) + target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build) print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) + cmds["build_py"] = cmd_build_py if "cx_Freeze" in sys.modules: # cx_freeze enabled? from cx_Freeze.dist import build_exe as _build_exe + # nczeczulin reports that py2exe won't like the pep440-style string # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. # setup(console=[{ @@ -1581,17 +1597,21 @@ def run(self): os.unlink(target_versionfile) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) + f.write( + LONG + % { + "DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + } + ) + cmds["build_exe"] = cmd_build_exe del cmds["build_py"] - if 'py2exe' in sys.modules: # py2exe enabled? + if "py2exe" in sys.modules: # py2exe enabled? try: from py2exe.distutils_buildexe import py2exe as _py2exe # py3 except ImportError: @@ -1610,13 +1630,17 @@ def run(self): os.unlink(target_versionfile) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) + f.write( + LONG + % { + "DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + } + ) + cmds["py2exe"] = cmd_py2exe # we override different "sdist" commands for both environments @@ -1643,8 +1667,8 @@ def make_release_tree(self, base_dir, files): # updated value target_versionfile = os.path.join(base_dir, cfg.versionfile_source) print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, - self._versioneer_generated_versions) + write_to_version_file(target_versionfile, self._versioneer_generated_versions) + cmds["sdist"] = cmd_sdist return cmds @@ -1699,11 +1723,9 @@ def do_setup(): root = get_root() try: cfg = get_config_from_root(root) - except (EnvironmentError, configparser.NoSectionError, - configparser.NoOptionError) as e: + except (EnvironmentError, configparser.NoSectionError, configparser.NoOptionError) as e: if isinstance(e, (EnvironmentError, configparser.NoSectionError)): - print("Adding sample versioneer config to setup.cfg", - file=sys.stderr) + print("Adding sample versioneer config to setup.cfg", file=sys.stderr) with open(os.path.join(root, "setup.cfg"), "a") as f: f.write(SAMPLE_CONFIG) print(CONFIG_ERROR, file=sys.stderr) @@ -1712,15 +1734,18 @@ def do_setup(): print(" creating %s" % cfg.versionfile_source) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - - ipy = os.path.join(os.path.dirname(cfg.versionfile_source), - "__init__.py") + f.write( + LONG + % { + "DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + } + ) + + ipy = os.path.join(os.path.dirname(cfg.versionfile_source), "__init__.py") if os.path.exists(ipy): try: with open(ipy, "r") as f: @@ -1762,8 +1787,7 @@ def do_setup(): else: print(" 'versioneer.py' already in MANIFEST.in") if cfg.versionfile_source not in simple_includes: - print(" appending versionfile_source ('%s') to MANIFEST.in" % - cfg.versionfile_source) + print(" appending versionfile_source ('%s') to MANIFEST.in" % cfg.versionfile_source) with open(manifest_in, "a") as f: f.write("include %s\n" % cfg.versionfile_source) else: From 6af97956719bb4afdc308c5d7f81115958841032 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 28 Jul 2022 21:22:06 -0700 Subject: [PATCH 5/5] fix references to server object --- tableauserverclient/models/site_item.py | 11 ++- .../server/endpoint/endpoint.py | 4 +- .../server/endpoint/jobs_endpoint.py | 2 +- .../server/endpoint/sites_endpoint.py | 2 +- .../server/endpoint/tasks_endpoint.py | 4 +- tableauserverclient/server/request_factory.py | 69 ++++++++++--------- tableauserverclient/server/server.py | 23 +++---- 7 files changed, 60 insertions(+), 55 deletions(-) diff --git a/tableauserverclient/models/site_item.py b/tableauserverclient/models/site_item.py index 3f6942d1d..3deda03e2 100644 --- a/tableauserverclient/models/site_item.py +++ b/tableauserverclient/models/site_item.py @@ -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, @@ -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): @@ -277,6 +280,10 @@ def cataloging_enabled(self, value: bool): 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 diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index cc3b35963..7e211cdf8 100644 --- a/tableauserverclient/server/endpoint/endpoint.py +++ b/tableauserverclient/server/endpoint/endpoint.py @@ -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: @@ -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 diff --git a/tableauserverclient/server/endpoint/jobs_endpoint.py b/tableauserverclient/server/endpoint/jobs_endpoint.py index 99870ac34..6b709efad 100644 --- a/tableauserverclient/server/endpoint/jobs_endpoint.py +++ b/tableauserverclient/server/endpoint/jobs_endpoint.py @@ -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) diff --git a/tableauserverclient/server/endpoint/sites_endpoint.py b/tableauserverclient/server/endpoint/sites_endpoint.py index 2797efbf0..67d7db209 100644 --- a/tableauserverclient/server/endpoint/sites_endpoint.py +++ b/tableauserverclient/server/endpoint/sites_endpoint.py @@ -90,7 +90,7 @@ def update(self, site_item: SiteItem) -> SiteItem: raise ValueError(error) url = "{0}/{1}".format(self.baseurl, site_item.id) - update_req = RequestFactory.Site.update_req(site_item) + update_req = RequestFactory.Site.update_req(site_item, self.parent_srv) server_response = self.put_request(url, update_req) logger.info("Updated site item (ID: {0})".format(site_item.id)) update_site = copy.copy(site_item) diff --git a/tableauserverclient/server/endpoint/tasks_endpoint.py b/tableauserverclient/server/endpoint/tasks_endpoint.py index f147c79ae..a70480b91 100644 --- a/tableauserverclient/server/endpoint/tasks_endpoint.py +++ b/tableauserverclient/server/endpoint/tasks_endpoint.py @@ -25,7 +25,7 @@ def __normalize_task_type(self, task_type): @api(version="2.6") def get(self, req_options=None, task_type=TaskItem.Type.ExtractRefresh): if task_type == TaskItem.Type.DataAcceleration: - self.parent_srv.assert_at_least_version("3.8") + self.parent_srv.assert_at_least_version("3.8", "Data Acceleration Tasks") logger.info("Querying all {} tasks for the site".format(task_type)) @@ -69,7 +69,7 @@ def run(self, task_item): @api(version="3.6") def delete(self, task_id, task_type=TaskItem.Type.ExtractRefresh): if task_type == TaskItem.Type.DataAcceleration: - self.parent_srv.assert_at_least_version("3.8") + self.parent_srv.assert_at_least_version("3.8", "Data Acceleration Tasks") if not task_id: error = "No Task ID provided" diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 01c4b66ac..cf3161c41 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -5,7 +5,6 @@ from requests.packages.urllib3.fields import RequestField from requests.packages.urllib3.filepost import encode_multipart_formdata -import tableauserverclient from tableauserverclient.models.metric_item import MetricItem from ..models import ConnectionItem @@ -26,6 +25,7 @@ from ..models import ConnectionItem from ..models import SiteItem from ..models import ProjectItem + from tableauserverclient.server import Server def _add_multipart(parts: Dict) -> Tuple[Any, str]: @@ -577,7 +577,7 @@ def add_flow_req(self, id_: Optional[str], task_type: str = TaskItem.Type.RunFlo class SiteRequest(object): - def update_req(self, site_item: "SiteItem"): + def update_req(self, site_item: "SiteItem", parent_srv: "Server" = None): xml_request = ET.Element("tsRequest") site_element = ET.SubElement(xml_request, "site") if site_item.name: @@ -602,14 +602,15 @@ def update_req(self, site_item: "SiteItem"): site_element.attrib["revisionHistoryEnabled"] = str(site_item.revision_history_enabled).lower() if site_item.data_acceleration_mode is not None: site_element.attrib["dataAccelerationMode"] = str(site_item.data_acceleration_mode).lower() - if site_item.flows_enabled is not None: - site_element.attrib["flowsEnabled"] = str(site_item.flows_enabled).lower() if site_item.cataloging_enabled is not None: site_element.attrib["catalogingEnabled"] = str(site_item.cataloging_enabled).lower() - if site_item.editing_flows_enabled is not None: - site_element.attrib["editingFlowsEnabled"] = str(site_item.editing_flows_enabled).lower() - if site_item.scheduling_flows_enabled is not None: - site_element.attrib["schedulingFlowsEnabled"] = str(site_item.scheduling_flows_enabled).lower() + + flows_edit = str(site_item.editing_flows_enabled).lower() + flows_schedule = str(site_item.scheduling_flows_enabled).lower() + flows_all = str(site_item.flows_enabled).lower() + + self.set_versioned_flow_attributes(flows_all, flows_edit, flows_schedule, parent_srv, site_element, site_item) + if site_item.allow_subscription_attachments is not None: site_element.attrib["allowSubscriptionAttachments"] = str(site_item.allow_subscription_attachments).lower() if site_item.guest_access_enabled is not None: @@ -684,7 +685,7 @@ def update_req(self, site_item: "SiteItem"): return ET.tostring(xml_request) # server: the site request model changes based on api version - def create_req(self, site_item: "SiteItem", parent_srv: object = None): + def create_req(self, site_item: "SiteItem", parent_srv: "Server" = None): xml_request = ET.Element("tsRequest") site_element = ET.SubElement(xml_request, "site") site_element.attrib["name"] = site_item.name @@ -707,30 +708,8 @@ def create_req(self, site_item: "SiteItem", parent_srv: object = None): flows_edit = str(site_item.editing_flows_enabled).lower() flows_schedule = str(site_item.scheduling_flows_enabled).lower() flows_all = str(site_item.flows_enabled).lower() - if parent_srv and parent_srv.check_at_least_version("3.10"): - if site_item.flows_enabled is not None: - flows_edit = flows_edit or flows_all - flows_schedule = flows_schedule or flows_all - import warnings - - warnings.warn( - "FlowsEnabled has been removed and become two options:" - " SchedulingFlowsEnabled and EditingFlowsEnabled" - ) - if site_item.editing_flows_enabled is not None: - site_element.attrib["editingFlowsEnabled"] = flows_edit - if site_item.scheduling_flows_enabled is not None: - site_element.attrib["schedulingFlowsEnabled"] = flows_schedule - else: # we default to using the older model - if site_item.flows_enabled is not None: - site_element.attrib["flowsEnabled"] = str(site_item.flows_enabled).lower() - if site_item.editing_flows_enabled is not None or site_item.scheduling_flows_enabled is not None: - flows_all = flows_all or flows_edit or flows_schedule - site_element.attrib["flowsEnabled"] = flows_all - import warnings - - warnings.warn("In version 3.10 and earlier there is only one option: FlowsEnabled") + self.set_versioned_flow_attributes(flows_all, flows_edit, flows_schedule, parent_srv, site_element, site_item) if site_item.allow_subscription_attachments is not None: site_element.attrib["allowSubscriptionAttachments"] = str(site_item.allow_subscription_attachments).lower() @@ -809,6 +788,32 @@ def create_req(self, site_item: "SiteItem", parent_srv: object = None): return ET.tostring(xml_request) + def set_versioned_flow_attributes(self, flows_all, flows_edit, flows_schedule, parent_srv, site_element, site_item): + if (not parent_srv) or SiteItem.use_new_flow_settings(parent_srv): + if site_item.flows_enabled is not None: + flows_edit = flows_edit or flows_all + flows_schedule = flows_schedule or flows_all + import warnings + + warnings.warn( + "FlowsEnabled has been removed and become two options:" + " SchedulingFlowsEnabled and EditingFlowsEnabled" + ) + if site_item.editing_flows_enabled is not None: + site_element.attrib["editingFlowsEnabled"] = flows_edit + if site_item.scheduling_flows_enabled is not None: + site_element.attrib["schedulingFlowsEnabled"] = flows_schedule + + else: + if site_item.flows_enabled is not None: + site_element.attrib["flowsEnabled"] = str(site_item.flows_enabled).lower() + if site_item.editing_flows_enabled is not None or site_item.scheduling_flows_enabled is not None: + flows_all = flows_all or flows_edit or flows_schedule + site_element.attrib["flowsEnabled"] = flows_all + import warnings + + warnings.warn("In version 3.10 and earlier there is only one option: FlowsEnabled") + class TableRequest(object): def update_req(self, table_item): diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index 6c5cdbf5e..8ac8c9ef2 100644 --- a/tableauserverclient/server/server.py +++ b/tableauserverclient/server/server.py @@ -1,6 +1,5 @@ from distutils.version import LooseVersion as Version import urllib3 -import requests from defusedxml.ElementTree import fromstring from .endpoint import ( @@ -30,8 +29,8 @@ Metrics, ) from .endpoint.exceptions import ( - EndpointUnavailableError, ServerInfoEndpointNotFoundError, + EndpointUnavailableError, ) from .exceptions import NotSignedInError from ..namespace import Namespace @@ -144,20 +143,14 @@ def use_highest_version(self): warnings.warn("use use_server_version instead", DeprecationWarning) - # sometimes a model changes in newer versions - def check_at_least_version(self, comparison: str): + def check_at_least_version(self, target: str): server_version = Version(self.version or "0.0") - compared_version = Version(comparison) - return not server_version < compared_version - - def assert_at_least_version(self, comparison: str): - if self.check_at_least_version(comparison): - return - else: - server_version = Version(self.version or "0.0") - error = "This endpoint (or model) is not available in API version {}. Requires {}".format( - server_version, comparison - ) + target_version = Version(target) + return server_version >= target_version + + def assert_at_least_version(self, comparison: str, reason: str): + if not self.check_at_least_version(comparison): + error = "{} is not available in API version {}. Requires {}".format(reason, self.version, comparison) raise EndpointUnavailableError(error) @property