diff --git a/.gitignore b/.gitignore index b3b3ff80f..6f8a86128 100644 --- a/.gitignore +++ b/.gitignore @@ -1,159 +1,161 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class -test.junit.xml - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg -pip-wheel-metadata/ - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# PyCharm stuff -.idea/ - -# IPython Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# poetry -poetry.lock - -# celery beat schedule file -celerybeat-schedule - -# dotenv -.env -env.py - -# virtualenv -venv/ -ENV/ -.venv/ - -# Spyder project settings -.spyderproject - -# Rope project settings -.ropeproject - -# VSCode project settings -.vscode/ - -# macOS.gitignore from https://github.com/github/gitignore -*.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - - - -# Windows.gitignore from https://github.com/github/gitignore -# Windows image file caches -Thumbs.db -ehthumbs.db - -# Folder config file -Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# Documentation -docs/_site/ -docs/.jekyll-metadata -docs/Gemfile.lock -samples/credentials -.venv/ +.claude/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +test.junit.xml + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg +pip-wheel-metadata/ + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# PyCharm stuff +.idea/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# poetry +poetry.lock + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env +env.py + +# virtualenv +venv/ +ENV/ +.venv/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# VSCode project settings +.vscode/ + +# macOS.gitignore from https://github.com/github/gitignore +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + + +# Windows.gitignore from https://github.com/github/gitignore +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Documentation +docs/_site/ +docs/.jekyll-metadata +docs/Gemfile.lock +samples/credentials +.venv/ diff --git a/pyproject.toml b/pyproject.toml index c832ffe71..ebb4fe8be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ classifiers = [ repository = "https://github.com/tableau/server-client-python" [project.optional-dependencies] -test = ["black==24.10", "build", "mypy==1.4", "pytest>=7.0", "pytest-cov", "pytest-subtests", +test = ["black==26.3.1", "build", "mypy==1.4", "pytest>=7.0", "pytest-cov", "pytest-subtests", "pytest-xdist", "requests-mock>=1.0,<2.0", "types-requests>=2.32.4.20250913"] [tool.setuptools.package-data] diff --git a/samples/export.py b/samples/export.py index b2506cf46..c7f1cdb06 100644 --- a/samples/export.py +++ b/samples/export.py @@ -1,107 +1,107 @@ -#### -# This script demonstrates how to export a view using the Tableau -# Server Client. -# -# To run the script, you must have installed Python 3.7 or later. -#### - -import argparse -import logging - -import tableauserverclient as TSC - - -def main(): - parser = argparse.ArgumentParser(description="Export a view as an image, PDF, or CSV") - # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", help="server address") - parser.add_argument("--site", "-S", help="site name") - parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") - parser.add_argument("--token-value", "-v", 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)", - ) - # Options specific to this sample - group = parser.add_mutually_exclusive_group(required=True) - group.add_argument( - "--pdf", dest="type", action="store_const", const=("populate_pdf", "PDFRequestOptions", "pdf", "pdf") - ) - group.add_argument( - "--png", dest="type", action="store_const", const=("populate_image", "ImageRequestOptions", "image", "png") - ) - group.add_argument( - "--csv", dest="type", action="store_const", const=("populate_csv", "CSVRequestOptions", "csv", "csv") - ) - # other options shown in explore_workbooks: workbook.download, workbook.preview_image - parser.add_argument( - "--language", help="Text such as 'Average' will appear in this language. Use values like fr, de, es, en" - ) - parser.add_argument("--workbook", action="store_true") - parser.add_argument("--custom_view", action="store_true") - - parser.add_argument("--file", "-f", help="filename to store the exported data") - parser.add_argument("--filter", "-vf", metavar="COLUMN:VALUE", help="View filter to apply to the view") - parser.add_argument("resource_id", help="LUID for the view or workbook") - - 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) - - tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site) - server = TSC.Server(args.server, use_server_version=True, http_options={"verify": False}) - with server.auth.sign_in(tableau_auth): - print("Connected") - if args.workbook: - item = server.workbooks.get_by_id(args.resource_id) - elif args.custom_view: - item = server.custom_views.get_by_id(args.resource_id) - else: - item = server.views.get_by_id(args.resource_id) - - if not item: - print(f"No item found for id {args.resource_id}") - exit(1) - - print(f"Item found: {item.name}") - # We have a number of different types and functions for each different export type. - # We encode that information above in the const=(...) parameter to the add_argument function to make - # the code automatically adapt for the type of export the user is doing. - # We unroll that information into methods we can call, or objects we can create by using getattr() - (populate_func_name, option_factory_name, member_name, extension) = args.type - populate = getattr(server.views, populate_func_name) - if args.workbook: - populate = getattr(server.workbooks, populate_func_name) - elif args.custom_view: - populate = getattr(server.custom_views, populate_func_name) - - option_factory = getattr(TSC, option_factory_name) - options: TSC.PDFRequestOptions = option_factory() - - if args.filter: - options = options.vf(*args.filter.split(":")) - - if args.language: - options.language = args.language - - if args.file: - filename = args.file - else: - filename = f"out-{options.language}.{extension}" - - populate(item, options) - with open(filename, "wb") as f: - if member_name == "csv": - f.writelines(getattr(item, member_name)) - else: - f.write(getattr(item, member_name)) - print("saved to " + filename) - - -if __name__ == "__main__": - main() +#### +# This script demonstrates how to export a view using the Tableau +# Server Client. +# +# To run the script, you must have installed Python 3.7 or later. +#### + +import argparse +import logging + +import tableauserverclient as TSC + + +def main(): + parser = argparse.ArgumentParser(description="Export a view as an image, PDF, or CSV") + # Common options; please keep those in sync across all samples + parser.add_argument("--server", "-s", help="server address") + parser.add_argument("--site", "-S", help="site name") + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", 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)", + ) + # Options specific to this sample + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + "--pdf", dest="type", action="store_const", const=("populate_pdf", "PDFRequestOptions", "pdf", "pdf") + ) + group.add_argument( + "--png", dest="type", action="store_const", const=("populate_image", "ImageRequestOptions", "image", "png") + ) + group.add_argument( + "--csv", dest="type", action="store_const", const=("populate_csv", "CSVRequestOptions", "csv", "csv") + ) + # other options shown in explore_workbooks: workbook.download, workbook.preview_image + parser.add_argument( + "--language", help="Text such as 'Average' will appear in this language. Use values like fr, de, es, en" + ) + parser.add_argument("--workbook", action="store_true") + parser.add_argument("--custom_view", action="store_true") + + parser.add_argument("--file", "-f", help="filename to store the exported data") + parser.add_argument("--filter", "-vf", metavar="COLUMN:VALUE", help="View filter to apply to the view") + parser.add_argument("resource_id", help="LUID for the view or workbook") + + 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) + + tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site) + server = TSC.Server(args.server, use_server_version=True, http_options={"verify": False}) + with server.auth.sign_in(tableau_auth): + print("Connected") + if args.workbook: + item = server.workbooks.get_by_id(args.resource_id) + elif args.custom_view: + item = server.custom_views.get_by_id(args.resource_id) + else: + item = server.views.get_by_id(args.resource_id) + + if not item: + print(f"No item found for id {args.resource_id}") + exit(1) + + print(f"Item found: {item.name}") + # We have a number of different types and functions for each different export type. + # We encode that information above in the const=(...) parameter to the add_argument function to make + # the code automatically adapt for the type of export the user is doing. + # We unroll that information into methods we can call, or objects we can create by using getattr() + populate_func_name, option_factory_name, member_name, extension = args.type + populate = getattr(server.views, populate_func_name) + if args.workbook: + populate = getattr(server.workbooks, populate_func_name) + elif args.custom_view: + populate = getattr(server.custom_views, populate_func_name) + + option_factory = getattr(TSC, option_factory_name) + options: TSC.PDFRequestOptions = option_factory() + + if args.filter: + options = options.vf(*args.filter.split(":")) + + if args.language: + options.language = args.language + + if args.file: + filename = args.file + else: + filename = f"out-{options.language}.{extension}" + + populate(item, options) + with open(filename, "wb") as f: + if member_name == "csv": + f.writelines(getattr(item, member_name)) + else: + f.write(getattr(item, member_name)) + print("saved to " + filename) + + +if __name__ == "__main__": + main() diff --git a/samples/smoke_test.py b/samples/smoke_test.py index b23eacdb8..237257014 100644 --- a/samples/smoke_test.py +++ b/samples/smoke_test.py @@ -1,16 +1,15 @@ -# This sample verifies that tableau server client is installed -# and you can run it. It also shows the version of the client. - -import logging -import tableauserverclient as TSC - - -logger = logging.getLogger("Sample") -logger.setLevel(logging.DEBUG) -logger.addHandler(logging.StreamHandler()) - - -server = TSC.Server("Fake-Server-Url", use_server_version=False) -print("Client details:") -logger.info(server.server_address) -logger.debug(TSC.server.endpoint.Endpoint.set_user_agent({})) +# This sample verifies that tableau server client is installed +# and you can run it. It also shows the version of the client. + +import logging +import tableauserverclient as TSC + +logger = logging.getLogger("Sample") +logger.setLevel(logging.DEBUG) +logger.addHandler(logging.StreamHandler()) + + +server = TSC.Server("Fake-Server-Url", use_server_version=False) +print("Client details:") +logger.info(server.server_address) +logger.debug(TSC.server.endpoint.Endpoint.set_user_agent({})) diff --git a/tableauserverclient/datetime_helpers.py b/tableauserverclient/datetime_helpers.py index 00f62faf8..e18db4c78 100644 --- a/tableauserverclient/datetime_helpers.py +++ b/tableauserverclient/datetime_helpers.py @@ -1,45 +1,44 @@ -import datetime - - -ZERO = datetime.timedelta(0) -HOUR = datetime.timedelta(hours=1) - - -def timestamp(): - return datetime.datetime.now().strftime("%H:%M:%S") - - -# 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""" - - def utcoffset(self, dt): - return ZERO - - def tzname(self, dt): - return "UTC" - - def dst(self, dt): - return ZERO - - -utc = UTC() -TABLEAU_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" - - -def parse_datetime(date): - if date is None: - return None - - try: - return datetime.datetime.strptime(date, TABLEAU_DATE_FORMAT).replace(tzinfo=utc) - except ValueError: - return None - - -def format_datetime(date): - if date is None: - return None - - return date.astimezone(tz=utc).strftime(TABLEAU_DATE_FORMAT) +import datetime + +ZERO = datetime.timedelta(0) +HOUR = datetime.timedelta(hours=1) + + +def timestamp(): + return datetime.datetime.now().strftime("%H:%M:%S") + + +# 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""" + + def utcoffset(self, dt): + return ZERO + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return ZERO + + +utc = UTC() +TABLEAU_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" + + +def parse_datetime(date): + if date is None: + return None + + try: + return datetime.datetime.strptime(date, TABLEAU_DATE_FORMAT).replace(tzinfo=utc) + except ValueError: + return None + + +def format_datetime(date): + if date is None: + return None + + return date.astimezone(tz=utc).strftime(TABLEAU_DATE_FORMAT) diff --git a/tableauserverclient/helpers/strings.py b/tableauserverclient/helpers/strings.py index 6ba4e48d9..7a0eaddc7 100644 --- a/tableauserverclient/helpers/strings.py +++ b/tableauserverclient/helpers/strings.py @@ -1,67 +1,66 @@ -from defusedxml.ElementTree import fromstring, tostring -from functools import singledispatch -from typing import TypeVar, overload - - -# the redact method can handle either strings or bytes, but it can't mix them. -# Generic type so we can write the actual logic once, then use singledispatch to -# create the replacement text with the correct type -T = TypeVar("T", str, bytes) - - -def _redact_any_type(xml: T, sensitive_word: T, replacement: T, encoding=None) -> T: - try: - root = fromstring(xml) - matches = root.findall(".//*[@password]") - for item in matches: - item.attrib["password"] = "********" - matches = root.findall(".//password") - for item in matches: - item.text = "********" - # tostring returns bytes unless an encoding value is passed - return tostring(root, encoding=encoding) - except Exception: - # something about the xml handling failed. Just cut off the text at the first occurrence of "password" - location = xml.find(sensitive_word) - return xml[:location] + replacement - - -@singledispatch -def redact_xml(content): - # this will only be called if it didn't get directed to the str or bytes overloads - raise TypeError("Redaction only works on xml saved as str or bytes") - - -@redact_xml.register -def _(xml: str) -> str: - out = _redact_any_type(xml, "password", "...[redacted]", encoding="unicode") - return out - - -@redact_xml.register # type: ignore[no-redef] -def _(xml: bytes) -> bytes: - return _redact_any_type(bytearray(xml), b"password", b"..[redacted]") - - -@overload -def nullable_str_to_int(value: None) -> None: ... - - -@overload -def nullable_str_to_int(value: str) -> int: ... - - -def nullable_str_to_int(value): - return int(value) if value is not None else None - - -@overload -def nullable_str_to_bool(value: None) -> None: ... - - -@overload -def nullable_str_to_bool(value: str) -> bool: ... - - -def nullable_str_to_bool(value): - return str(value).lower() == "true" if value is not None else None +from defusedxml.ElementTree import fromstring, tostring +from functools import singledispatch +from typing import TypeVar, overload + +# the redact method can handle either strings or bytes, but it can't mix them. +# Generic type so we can write the actual logic once, then use singledispatch to +# create the replacement text with the correct type +T = TypeVar("T", str, bytes) + + +def _redact_any_type(xml: T, sensitive_word: T, replacement: T, encoding=None) -> T: + try: + root = fromstring(xml) + matches = root.findall(".//*[@password]") + for item in matches: + item.attrib["password"] = "********" + matches = root.findall(".//password") + for item in matches: + item.text = "********" + # tostring returns bytes unless an encoding value is passed + return tostring(root, encoding=encoding) + except Exception: + # something about the xml handling failed. Just cut off the text at the first occurrence of "password" + location = xml.find(sensitive_word) + return xml[:location] + replacement + + +@singledispatch +def redact_xml(content): + # this will only be called if it didn't get directed to the str or bytes overloads + raise TypeError("Redaction only works on xml saved as str or bytes") + + +@redact_xml.register +def _(xml: str) -> str: + out = _redact_any_type(xml, "password", "...[redacted]", encoding="unicode") + return out + + +@redact_xml.register # type: ignore[no-redef] +def _(xml: bytes) -> bytes: + return _redact_any_type(bytearray(xml), b"password", b"..[redacted]") + + +@overload +def nullable_str_to_int(value: None) -> None: ... + + +@overload +def nullable_str_to_int(value: str) -> int: ... + + +def nullable_str_to_int(value): + return int(value) if value is not None else None + + +@overload +def nullable_str_to_bool(value: None) -> None: ... + + +@overload +def nullable_str_to_bool(value: str) -> bool: ... + + +def nullable_str_to_bool(value): + return str(value).lower() == "true" if value is not None else None diff --git a/tableauserverclient/models/column_item.py b/tableauserverclient/models/column_item.py index 3a7416e28..739b7e8ef 100644 --- a/tableauserverclient/models/column_item.py +++ b/tableauserverclient/models/column_item.py @@ -1,71 +1,71 @@ -from defusedxml.ElementTree import fromstring - -from .property_decorators import property_not_empty - - -class ColumnItem: - def __init__(self, name, description=None): - self._id = None - self.description = description - self.name = name - - def __repr__(self): - return f"<{self.__class__.__name__} {self._id} {self.name} {self.description}>" - - @property - def id(self): - return self._id - - @property - def name(self): - return self._name - - @name.setter - @property_not_empty - def name(self, value): - self._name = value - - @property - def description(self): - return self._description - - @description.setter - def description(self, value): - self._description = value - - @property - def remote_type(self): - return self._remote_type - - def _set_values(self, id, name, description, remote_type): - if id is not None: - self._id = id - if name: - self._name = name - if description: - self.description = description - if remote_type: - self._remote_type = remote_type - - @classmethod - def from_response(cls, resp, ns): - all_column_items = list() - parsed_response = fromstring(resp) - all_column_xml = parsed_response.findall(".//t:column", namespaces=ns) - - for column_xml in all_column_xml: - (id, name, description, remote_type) = cls._parse_element(column_xml, ns) - column_item = cls(name) - column_item._set_values(id, name, description, remote_type) - all_column_items.append(column_item) - - return all_column_items - - @staticmethod - def _parse_element(column_xml, ns): - id = column_xml.get("id", None) - name = column_xml.get("name", None) - description = column_xml.get("description", None) - remote_type = column_xml.get("remoteType", None) - - return id, name, description, remote_type +from defusedxml.ElementTree import fromstring + +from .property_decorators import property_not_empty + + +class ColumnItem: + def __init__(self, name, description=None): + self._id = None + self.description = description + self.name = name + + def __repr__(self): + return f"<{self.__class__.__name__} {self._id} {self.name} {self.description}>" + + @property + def id(self): + return self._id + + @property + def name(self): + return self._name + + @name.setter + @property_not_empty + def name(self, value): + self._name = value + + @property + def description(self): + return self._description + + @description.setter + def description(self, value): + self._description = value + + @property + def remote_type(self): + return self._remote_type + + def _set_values(self, id, name, description, remote_type): + if id is not None: + self._id = id + if name: + self._name = name + if description: + self.description = description + if remote_type: + self._remote_type = remote_type + + @classmethod + def from_response(cls, resp, ns): + all_column_items = list() + parsed_response = fromstring(resp) + all_column_xml = parsed_response.findall(".//t:column", namespaces=ns) + + for column_xml in all_column_xml: + id, name, description, remote_type = cls._parse_element(column_xml, ns) + column_item = cls(name) + column_item._set_values(id, name, description, remote_type) + all_column_items.append(column_item) + + return all_column_items + + @staticmethod + def _parse_element(column_xml, ns): + id = column_xml.get("id", None) + name = column_xml.get("name", None) + description = column_xml.get("description", None) + remote_type = column_xml.get("remoteType", None) + + return id, name, description, remote_type diff --git a/tableauserverclient/models/data_alert_item.py b/tableauserverclient/models/data_alert_item.py index ff62c3ae8..02ebb6409 100644 --- a/tableauserverclient/models/data_alert_item.py +++ b/tableauserverclient/models/data_alert_item.py @@ -37,9 +37,7 @@ def __init__(self): def __repr__(self) -> str: return "".format( - **self.__dict__ - ) + public={_public}>".format(**self.__dict__) @property def id(self) -> str | None: diff --git a/tableauserverclient/models/subscription_item.py b/tableauserverclient/models/subscription_item.py index 61c75e2d6..f53a1e0cf 100644 --- a/tableauserverclient/models/subscription_item.py +++ b/tableauserverclient/models/subscription_item.py @@ -1,144 +1,140 @@ -from typing import TYPE_CHECKING - -from defusedxml.ElementTree import fromstring - -from .property_decorators import property_is_boolean -from .target import Target -from tableauserverclient.models import ScheduleItem - -if TYPE_CHECKING: - from .target import Target - - -class SubscriptionItem: - def __init__(self, subject: str, schedule_id: str, user_id: str, target: "Target") -> None: - self._id = None - self.attach_image = True - self.attach_pdf = False - self.message = None - self.page_orientation = None - self.page_size_option = None - self.schedule_id = schedule_id - self.send_if_view_empty = True - self.subject = subject - self.suspended = False - self.target = target - self.user_id = user_id - self.schedule = None - - def __repr__(self) -> str: - if self.id is not None: - return " bool: - return self._attach_image - - @attach_image.setter - @property_is_boolean - def attach_image(self, value: bool): - self._attach_image = value - - @property - def attach_pdf(self) -> bool: - return self._attach_pdf - - @attach_pdf.setter - @property_is_boolean - def attach_pdf(self, value: bool) -> None: - self._attach_pdf = value - - @property - def send_if_view_empty(self) -> bool: - return self._send_if_view_empty - - @send_if_view_empty.setter - @property_is_boolean - def send_if_view_empty(self, value: bool) -> None: - self._send_if_view_empty = value - - @property - def suspended(self) -> bool: - return self._suspended - - @suspended.setter - @property_is_boolean - def suspended(self, value: bool) -> None: - self._suspended = value - - @classmethod - def from_response(cls: type, xml: bytes, ns) -> list["SubscriptionItem"]: - parsed_response = fromstring(xml) - all_subscriptions_xml = parsed_response.findall(".//t:subscription", namespaces=ns) - - all_subscriptions = [SubscriptionItem._parse_element(x, ns) for x in all_subscriptions_xml] - return all_subscriptions - - @classmethod - def _parse_element(cls, element, ns): - schedule_element = element.find(".//t:schedule", namespaces=ns) - content_element = element.find(".//t:content", namespaces=ns) - user_element = element.find(".//t:user", namespaces=ns) - - # Schedule element - schedule_id = None - schedule = None - if schedule_element is not None: - schedule_id = schedule_element.get("id", None) - - # If schedule id is not provided, then TOL with full schedule provided - if schedule_id is None: - schedule = ScheduleItem.from_element(element, ns) - - # Content element - target = None - send_if_view_empty = None - if content_element is not None: - target = Target(content_element.get("id", None), content_element.get("type")) - send_if_view_empty = string_to_bool(content_element.get("sendIfViewEmpty", "")) - - # User element - user_id = None - if user_element is not None: - user_id = user_element.get("id", None) - - # Main attributes - id_ = element.get("id", None) - subject = element.get("subject", None) - attach_image = string_to_bool(element.get("attachImage", "")) - attach_pdf = string_to_bool(element.get("attachPdf", "")) - message = element.get("message", None) - page_orientation = element.get("pageOrientation", None) - page_size_option = element.get("pageSizeOption", None) - suspended = string_to_bool(element.get("suspended", "")) - - # Create SubscriptionItem and set fields - sub = cls(subject, schedule_id, user_id, target) - sub._id = id_ - sub.attach_image = attach_image - sub.attach_pdf = attach_pdf - sub.message = message - sub.page_orientation = page_orientation - sub.page_size_option = page_size_option - sub.send_if_view_empty = send_if_view_empty - sub.suspended = suspended - sub.schedule = schedule - - return sub - - -# Used to convert string represented boolean to a boolean type -def string_to_bool(s: str) -> bool: - return s.lower() == "true" +from typing import TYPE_CHECKING + +from defusedxml.ElementTree import fromstring + +from .property_decorators import property_is_boolean +from .target import Target +from tableauserverclient.models import ScheduleItem + +if TYPE_CHECKING: + from .target import Target + + +class SubscriptionItem: + def __init__(self, subject: str, schedule_id: str, user_id: str, target: "Target") -> None: + self._id = None + self.attach_image = True + self.attach_pdf = False + self.message = None + self.page_orientation = None + self.page_size_option = None + self.schedule_id = schedule_id + self.send_if_view_empty = True + self.subject = subject + self.suspended = False + self.target = target + self.user_id = user_id + self.schedule = None + + def __repr__(self) -> str: + if self.id is not None: + return " bool: + return self._attach_image + + @attach_image.setter + @property_is_boolean + def attach_image(self, value: bool): + self._attach_image = value + + @property + def attach_pdf(self) -> bool: + return self._attach_pdf + + @attach_pdf.setter + @property_is_boolean + def attach_pdf(self, value: bool) -> None: + self._attach_pdf = value + + @property + def send_if_view_empty(self) -> bool: + return self._send_if_view_empty + + @send_if_view_empty.setter + @property_is_boolean + def send_if_view_empty(self, value: bool) -> None: + self._send_if_view_empty = value + + @property + def suspended(self) -> bool: + return self._suspended + + @suspended.setter + @property_is_boolean + def suspended(self, value: bool) -> None: + self._suspended = value + + @classmethod + def from_response(cls: type, xml: bytes, ns) -> list["SubscriptionItem"]: + parsed_response = fromstring(xml) + all_subscriptions_xml = parsed_response.findall(".//t:subscription", namespaces=ns) + + all_subscriptions = [SubscriptionItem._parse_element(x, ns) for x in all_subscriptions_xml] + return all_subscriptions + + @classmethod + def _parse_element(cls, element, ns): + schedule_element = element.find(".//t:schedule", namespaces=ns) + content_element = element.find(".//t:content", namespaces=ns) + user_element = element.find(".//t:user", namespaces=ns) + + # Schedule element + schedule_id = None + schedule = None + if schedule_element is not None: + schedule_id = schedule_element.get("id", None) + + # If schedule id is not provided, then TOL with full schedule provided + if schedule_id is None: + schedule = ScheduleItem.from_element(element, ns) + + # Content element + target = None + send_if_view_empty = None + if content_element is not None: + target = Target(content_element.get("id", None), content_element.get("type")) + send_if_view_empty = string_to_bool(content_element.get("sendIfViewEmpty", "")) + + # User element + user_id = None + if user_element is not None: + user_id = user_element.get("id", None) + + # Main attributes + id_ = element.get("id", None) + subject = element.get("subject", None) + attach_image = string_to_bool(element.get("attachImage", "")) + attach_pdf = string_to_bool(element.get("attachPdf", "")) + message = element.get("message", None) + page_orientation = element.get("pageOrientation", None) + page_size_option = element.get("pageSizeOption", None) + suspended = string_to_bool(element.get("suspended", "")) + + # Create SubscriptionItem and set fields + sub = cls(subject, schedule_id, user_id, target) + sub._id = id_ + sub.attach_image = attach_image + sub.attach_pdf = attach_pdf + sub.message = message + sub.page_orientation = page_orientation + sub.page_size_option = page_size_option + sub.send_if_view_empty = send_if_view_empty + sub.suspended = suspended + sub.schedule = schedule + + return sub + + +# Used to convert string represented boolean to a boolean type +def string_to_bool(s: str) -> bool: + return s.lower() == "true" diff --git a/tableauserverclient/server/endpoint/data_alert_endpoint.py b/tableauserverclient/server/endpoint/data_alert_endpoint.py index ec7d76c9c..f58d06e41 100644 --- a/tableauserverclient/server/endpoint/data_alert_endpoint.py +++ b/tableauserverclient/server/endpoint/data_alert_endpoint.py @@ -9,7 +9,6 @@ from typing import TYPE_CHECKING - if TYPE_CHECKING: from ..server import Server from ..request_options import RequestOptions diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index 941b6eee6..321d0120b 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -226,7 +226,7 @@ def add_to_schedule( task_type = TaskItem.Type.RunFlow items.append( (schedule_id, flow, "flow", RequestFactory.Schedule.add_flow_req, task_type) - ) # type:ignore[arg-type] + ) # type: ignore[arg-type] results = (self._add_to(*x) for x in items) return [x for x in results if not x.result] diff --git a/tableauserverclient/server/pager.py b/tableauserverclient/server/pager.py index ec1425d6f..be3802ddf 100644 --- a/tableauserverclient/server/pager.py +++ b/tableauserverclient/server/pager.py @@ -6,7 +6,6 @@ from tableauserverclient.models.pagination_item import PaginationItem from tableauserverclient.server.request_options import RequestOptions - T = TypeVar("T") diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index f2e1db382..e22e43e2f 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -611,7 +611,7 @@ def update_req(self, schedule_item): intervals_element = ET.SubElement(frequency_element, "intervals") if hasattr(interval_item, "interval"): for interval in interval_item._interval_type_pairs(): - (expression, value) = interval + expression, value = interval single_interval_element = ET.SubElement(intervals_element, "interval") single_interval_element.attrib[expression] = value return ET.tostring(xml_request) diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index b497e9086..856387cea 100644 --- a/tableauserverclient/server/server.py +++ b/tableauserverclient/server/server.py @@ -48,7 +48,6 @@ from tableauserverclient.server.endpoint.exceptions import NotSignedInError from tableauserverclient.namespace import Namespace - _PRODUCT_TO_REST_VERSION = { "10.0": "2.3", "9.3": "2.2", diff --git a/test/test_extensions.py b/test/test_extensions.py index 9dc001876..7f6f84b93 100644 --- a/test/test_extensions.py +++ b/test/test_extensions.py @@ -6,7 +6,6 @@ import tableauserverclient as TSC - TEST_ASSET_DIR = Path(__file__).parent / "assets" GET_SERVER_EXT_SETTINGS = TEST_ASSET_DIR / "extensions_server_settings_true.xml" diff --git a/test/test_job.py b/test/test_job.py index fa17b9953..19f324d1e 100644 --- a/test/test_job.py +++ b/test/test_job.py @@ -9,7 +9,6 @@ from tableauserverclient.server.endpoint.exceptions import JobFailedException from ._utils import mocked_time - TEST_ASSET_DIR = Path(__file__).parent / "assets" GET_XML = TEST_ASSET_DIR / "job_get.xml" GET_BY_ID_XML = TEST_ASSET_DIR / "job_get_by_id.xml"