diff --git a/contributing.md b/contributing.md index c7f487ec3..3d5cd3d43 100644 --- a/contributing.md +++ b/contributing.md @@ -67,5 +67,9 @@ python setup.py test Our CI runs include a Python lint run, so you should run this locally and fix complaints before committing as this will fail your checkin. ```shell -pycodestyle tableauserverclient test samples +# this will run the formatter without making changes +black --line-length 120 tableauserverclient --check + +# this will format the directory and code for you +black --line-length 120 tableauserverclient ``` diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py index 2eadcdfa1..fcce4e0c7 100644 --- a/tableauserverclient/__init__.py +++ b/tableauserverclient/__init__.py @@ -4,6 +4,7 @@ ConnectionItem, DataAlertItem, DatasourceItem, + DQWItem, GroupItem, JobItem, BackgroundJobItem, diff --git a/tableauserverclient/_version.py b/tableauserverclient/_version.py index c8afb10d4..5e73890bd 100644 --- a/tableauserverclient/_version.py +++ b/tableauserverclient/_version.py @@ -77,7 +77,11 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= 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) + [c] + args, + cwd=cwd, + env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr else None), ) break except EnvironmentError: @@ -243,7 +247,17 @@ 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 + 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: @@ -285,7 +299,10 @@ 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) :] diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py index dff12a29d..c0ddc2e75 100644 --- a/tableauserverclient/models/__init__.py +++ b/tableauserverclient/models/__init__.py @@ -5,11 +5,18 @@ from .data_alert_item import DataAlertItem from .datasource_item import DatasourceItem from .database_item import DatabaseItem +from .dqw_item import DQWItem from .exceptions import UnpopulatedPropertyError from .favorites_item import FavoriteItem from .group_item import GroupItem from .flow_item import FlowItem -from .interval_item import IntervalItem, DailyInterval, WeeklyInterval, MonthlyInterval, HourlyInterval +from .interval_item import ( + IntervalItem, + DailyInterval, + WeeklyInterval, + MonthlyInterval, + HourlyInterval, +) from .job_item import JobItem, BackgroundJobItem from .pagination_item import PaginationItem from .project_item import ProjectItem diff --git a/tableauserverclient/models/data_alert_item.py b/tableauserverclient/models/data_alert_item.py index c924b6ab2..d719469b0 100644 --- a/tableauserverclient/models/data_alert_item.py +++ b/tableauserverclient/models/data_alert_item.py @@ -1,6 +1,10 @@ import xml.etree.ElementTree as ET -from .property_decorators import property_not_empty, property_is_enum, property_is_boolean +from .property_decorators import ( + property_not_empty, + property_is_enum, + property_is_boolean, +) from .user_item import UserItem from .view_item import ViewItem diff --git a/tableauserverclient/models/database_item.py b/tableauserverclient/models/database_item.py index a319606e4..4934af81b 100644 --- a/tableauserverclient/models/database_item.py +++ b/tableauserverclient/models/database_item.py @@ -1,6 +1,10 @@ import xml.etree.ElementTree as ET -from .property_decorators import property_is_enum, property_not_empty, property_is_boolean +from .property_decorators import ( + property_is_enum, + property_not_empty, + property_is_boolean, +) from .exceptions import UnpopulatedPropertyError @@ -34,8 +38,17 @@ def __init__(self, name, description=None, content_permissions=None): self._permissions = None self._default_table_permissions = None + self._data_quality_warnings = None + self._tables = None # Not implemented yet + @property + def dqws(self): + if self._data_quality_warnings is None: + error = "Project item must be populated with permissions first." + raise UnpopulatedPropertyError(error) + return self._data_quality_warnings() + @property def content_permissions(self): return self._content_permissions @@ -229,7 +242,14 @@ def _set_tables(self, tables): self._tables = tables def _set_default_permissions(self, permissions, content_type): - setattr(self, "_default_{content}_permissions".format(content=content_type), permissions) + setattr( + self, + "_default_{content}_permissions".format(content=content_type), + permissions, + ) + + def _set_data_quality_warnings(self, dqw): + self._data_quality_warnings = dqw @classmethod def from_response(cls, resp, ns): diff --git a/tableauserverclient/models/datasource_item.py b/tableauserverclient/models/datasource_item.py index 219df39c2..78c2a44ca 100644 --- a/tableauserverclient/models/datasource_item.py +++ b/tableauserverclient/models/datasource_item.py @@ -1,6 +1,10 @@ import xml.etree.ElementTree as ET from .exceptions import UnpopulatedPropertyError -from .property_decorators import property_not_nullable, property_is_boolean, property_is_enum +from .property_decorators import ( + property_not_nullable, + property_is_boolean, + property_is_enum, +) from .tag_item import TagItem from ..datetime_helpers import parse_datetime import copy @@ -35,6 +39,7 @@ def __init__(self, project_id, name=None): self.tags = set() self._permissions = None + self._data_quality_warnings = None @property def ask_data_enablement(self): @@ -94,6 +99,13 @@ def encrypt_extracts(self): def encrypt_extracts(self, value): self._encrypt_extracts = value + @property + def dqws(self): + if self._data_quality_warnings is None: + error = "Project item must be populated with dqws first." + raise UnpopulatedPropertyError(error) + return self._data_quality_warnings() + @property def has_extracts(self): return self._has_extracts @@ -142,6 +154,9 @@ def _set_connections(self, connections): def _set_permissions(self, permissions): self._permissions = permissions + def _set_data_quality_warnings(self, dqws): + self._data_quality_warnings = dqws + def _parse_common_elements(self, datasource_xml, ns): if not isinstance(datasource_xml, ET.Element): datasource_xml = ET.fromstring(datasource_xml).find(".//t:datasource", namespaces=ns) diff --git a/tableauserverclient/models/dqw_item.py b/tableauserverclient/models/dqw_item.py new file mode 100644 index 000000000..a7f8ec9cb --- /dev/null +++ b/tableauserverclient/models/dqw_item.py @@ -0,0 +1,148 @@ +import xml.etree.ElementTree as ET +from ..datetime_helpers import parse_datetime + + +class DQWItem(object): + class WarningType: + WARNING = "WARNING" + DEPRECATED = "DEPRECATED" + STALE = "STALE" + SENSITIVE_DATA = "SENSITIVE_DATA" + MAINTENANCE = "MAINTENANCE" + + def __init__(self, warning_type="WARNING", message=None, active=True, severe=False): + self._id = None + # content related + self._content_id = None + self._content_type = None + + # DQW related + self.warning_type = warning_type + self.message = message + self.active = active + self.severe = severe + self._created_at = None + self._updated_at = None + + # owner + self._owner_display_name = None + self._owner_id = None + + @property + def id(self): + return self._id + + @property + def content_id(self): + return self._content_id + + @property + def content_type(self): + return self._content_type + + @property + def owner_display_name(self): + return self._owner_display_name + + @property + def owner_id(self): + return self._owner_id + + @property + def warning_type(self): + return self._warning_type + + @warning_type.setter + def warning_type(self, value): + self._warning_type = value + + @property + def message(self): + return self._message + + @message.setter + def message(self, value): + self._message = value + + @property + def active(self): + return self._active + + @active.setter + def active(self, value): + self._active = value + + @property + def severe(self): + return self._severe + + @severe.setter + def severe(self, value): + self._severe = value + + @property + def active(self): + return self._active + + @active.setter + def active(self, value): + self._active = value + + @property + def created_at(self): + return self._created_at + + @created_at.setter + def created_at(self, value): + self._created_at = value + + @property + def updated_at(self): + return self._updated_at + + @updated_at.setter + def updated_at(self, value): + self._updated_at = value + + @classmethod + def from_response(cls, resp, ns): + return cls.from_xml_element(ET.fromstring(resp), ns) + + @classmethod + def from_xml_element(cls, parsed_response, ns): + all_dqws = [] + dqw_elem_list = parsed_response.findall(".//t:dataQualityWarning", namespaces=ns) + for dqw_elem in dqw_elem_list: + dqw = DQWItem() + dqw._id = dqw_elem.get("id", None) + dqw._owner_display_name = dqw_elem.get("userDisplayName", None) + dqw._content_id = dqw_elem.get("contentId", None) + dqw._content_type = dqw_elem.get("contentType", None) + dqw.message = dqw_elem.get("message", None) + dqw.warning_type = dqw_elem.get("type", None) + + is_active = dqw_elem.get("isActive", None) + if is_active is not None: + dqw._active = string_to_bool(is_active) + + is_severe = dqw_elem.get("isSevere", None) + if is_severe is not None: + dqw._severe = string_to_bool(is_severe) + + dqw._created_at = parse_datetime(dqw_elem.get("createdAt", None)) + dqw._updated_at = parse_datetime(dqw_elem.get("updatedAt", None)) + + owner_id = None + owner_tag = dqw_elem.find(".//t:owner", namespaces=ns) + if owner_tag is not None: + owner_id = owner_tag.get("id", None) + dqw._owner_id = owner_id + + all_dqws.append(dqw) + + return all_dqws + + +# Used to convert string represented boolean to a boolean type +def string_to_bool(s): + return s.lower() == "true" diff --git a/tableauserverclient/models/flow_item.py b/tableauserverclient/models/flow_item.py index 99e857369..d1387f368 100644 --- a/tableauserverclient/models/flow_item.py +++ b/tableauserverclient/models/flow_item.py @@ -22,6 +22,7 @@ def __init__(self, project_id, name=None): self._connections = None self._permissions = None + self._data_quality_warnings = None @property def connections(self): @@ -45,6 +46,13 @@ def webpage_url(self): def created_at(self): return self._created_at + @property + def dqws(self): + if self._data_quality_warnings is None: + error = "Project item must be populated with dqws first." + raise UnpopulatedPropertyError(error) + return self._data_quality_warnings() + @property def id(self): return self._id @@ -84,16 +92,51 @@ def _set_connections(self, connections): def _set_permissions(self, permissions): self._permissions = permissions + def _set_data_quality_warnings(self, dqws): + self._data_quality_warnings = dqws + def _parse_common_elements(self, flow_xml, ns): if not isinstance(flow_xml, ET.Element): flow_xml = ET.fromstring(flow_xml).find(".//t:flow", namespaces=ns) if flow_xml is not None: - (_, _, _, _, _, updated_at, _, project_id, project_name, owner_id) = self._parse_element(flow_xml, ns) - self._set_values(None, None, None, None, None, updated_at, None, project_id, project_name, owner_id) + ( + _, + _, + _, + _, + _, + updated_at, + _, + project_id, + project_name, + owner_id, + ) = self._parse_element(flow_xml, ns) + self._set_values( + None, + None, + None, + None, + None, + updated_at, + None, + project_id, + project_name, + owner_id, + ) return self def _set_values( - self, id, name, description, webpage_url, created_at, updated_at, tags, project_id, project_name, owner_id + self, + id, + name, + description, + webpage_url, + created_at, + updated_at, + tags, + project_id, + project_name, + owner_id, ): if id is not None: self._id = id @@ -138,7 +181,16 @@ def from_response(cls, resp, ns): ) = cls._parse_element(flow_xml, ns) flow_item = cls(project_id) flow_item._set_values( - id_, name, description, webpage_url, created_at, updated_at, tags, None, project_name, owner_id + id_, + name, + description, + webpage_url, + created_at, + updated_at, + tags, + None, + project_name, + owner_id, ) all_flow_items.append(flow_item) return all_flow_items @@ -169,4 +221,15 @@ def _parse_element(flow_xml, ns): if owner_elem is not None: owner_id = owner_elem.get("id", None) - return (id_, name, description, webpage_url, created_at, updated_at, tags, project_id, project_name, owner_id) + return ( + id_, + name, + description, + webpage_url, + created_at, + updated_at, + tags, + project_id, + project_name, + owner_id, + ) diff --git a/tableauserverclient/models/job_item.py b/tableauserverclient/models/job_item.py index 7b7ea4921..7a3a50861 100644 --- a/tableauserverclient/models/job_item.py +++ b/tableauserverclient/models/job_item.py @@ -92,7 +92,17 @@ def _parse_element(cls, element, ns): finish_code = element.get("finishCode", -1) notes = [note.text for note in element.findall(".//t:notes", namespaces=ns)] or None mode = element.get("mode", None) - return cls(id_, type_, progress, created_at, started_at, completed_at, finish_code, notes, mode) + return cls( + id_, + type_, + progress, + created_at, + started_at, + completed_at, + finish_code, + notes, + mode, + ) class BackgroundJobItem(object): @@ -104,7 +114,16 @@ class Status: Cancelled = "Cancelled" def __init__( - self, id_, created_at, priority, job_type, status, title=None, subtitle=None, started_at=None, ended_at=None + self, + id_, + created_at, + priority, + job_type, + status, + title=None, + subtitle=None, + started_at=None, + ended_at=None, ): self._id = id_ self._type = job_type @@ -176,4 +195,14 @@ def _parse_element(cls, element, ns): title = element.get("title", None) subtitle = element.get("subtitle", None) - return cls(id_, created_at, priority, type_, status, title, subtitle, started_at, ended_at) + return cls( + id_, + created_at, + priority, + type_, + status, + title, + subtitle, + started_at, + ended_at, + ) diff --git a/tableauserverclient/models/personal_access_token_auth.py b/tableauserverclient/models/personal_access_token_auth.py index c80a020e8..a95972164 100644 --- a/tableauserverclient/models/personal_access_token_auth.py +++ b/tableauserverclient/models/personal_access_token_auth.py @@ -8,7 +8,10 @@ def __init__(self, token_name, personal_access_token, site_id=""): @property def credentials(self): - return {"personalAccessTokenName": self.token_name, "personalAccessTokenSecret": self.personal_access_token} + return { + "personalAccessTokenName": self.token_name, + "personalAccessTokenSecret": self.personal_access_token, + } def __repr__(self): return "".format(self.token_name, self.personal_access_token) diff --git a/tableauserverclient/models/project_item.py b/tableauserverclient/models/project_item.py index e8434a0ad..bed6def6e 100644 --- a/tableauserverclient/models/project_item.py +++ b/tableauserverclient/models/project_item.py @@ -90,7 +90,13 @@ def _parse_common_tags(self, project_xml, ns): project_xml = ET.fromstring(project_xml).find(".//t:project", namespaces=ns) if project_xml is not None: - (_, name, description, content_permissions, parent_id) = self._parse_element(project_xml) + ( + _, + name, + description, + content_permissions, + parent_id, + ) = self._parse_element(project_xml) self._set_values(None, name, description, content_permissions, parent_id) return self @@ -112,7 +118,11 @@ def _set_permissions(self, permissions): self._permissions = permissions def _set_default_permissions(self, permissions, content_type): - setattr(self, "_default_{content}_permissions".format(content=content_type), permissions) + setattr( + self, + "_default_{content}_permissions".format(content=content_type), + permissions, + ) @classmethod def from_response(cls, resp, ns): @@ -121,7 +131,14 @@ def from_response(cls, resp, ns): all_project_xml = parsed_response.findall(".//t:project", namespaces=ns) for project_xml in all_project_xml: - (id, name, description, content_permissions, parent_id, owner_id) = cls._parse_element(project_xml) + ( + id, + name, + description, + content_permissions, + parent_id, + owner_id, + ) = cls._parse_element(project_xml) project_item = cls(name) project_item._set_values(id, name, description, content_permissions, parent_id, owner_id) all_project_items.append(project_item) diff --git a/tableauserverclient/models/property_decorators.py b/tableauserverclient/models/property_decorators.py index 153786d4c..b3466dea7 100644 --- a/tableauserverclient/models/property_decorators.py +++ b/tableauserverclient/models/property_decorators.py @@ -152,7 +152,12 @@ def wrapper(self, value): raise ValueError("{} is not type 'dict', cannot update {})".format(value.__class__.__name__, func.__name__)) if len(value) != 4 or not all( attr in value.keys() - for attr in ("acceleration_enabled", "accelerate_now", "last_updated_at", "acceleration_status") + for attr in ( + "acceleration_enabled", + "accelerate_now", + "last_updated_at", + "acceleration_status", + ) ): error = "{} should have 2 keys ".format(func.__name__) error += "'acceleration_enabled' and 'accelerate_now'" diff --git a/tableauserverclient/models/schedule_item.py b/tableauserverclient/models/schedule_item.py index b54c20ae9..f8baf0749 100644 --- a/tableauserverclient/models/schedule_item.py +++ b/tableauserverclient/models/schedule_item.py @@ -1,8 +1,18 @@ import xml.etree.ElementTree as ET from datetime import datetime -from .interval_item import IntervalItem, HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval -from .property_decorators import property_is_enum, property_not_nullable, property_is_int +from .interval_item import ( + IntervalItem, + HourlyInterval, + DailyInterval, + WeeklyInterval, + MonthlyInterval, +) +from .property_decorators import ( + property_is_enum, + property_not_nullable, + property_is_int, +) from ..datetime_helpers import parse_datetime diff --git a/tableauserverclient/models/site_item.py b/tableauserverclient/models/site_item.py index ac20e6a89..7fb1d116e 100644 --- a/tableauserverclient/models/site_item.py +++ b/tableauserverclient/models/site_item.py @@ -138,7 +138,10 @@ def content_url(self): @content_url.setter @property_not_nullable - @property_matches(VALID_CONTENT_URL_RE, "content_url can contain only letters, numbers, dashes, and underscores") + @property_matches( + VALID_CONTENT_URL_RE, + "content_url can contain only letters, numbers, dashes, and underscores", + ) def content_url(self, value): self._content_url = value diff --git a/tableauserverclient/models/table_item.py b/tableauserverclient/models/table_item.py index 16f43b98e..2f47400f7 100644 --- a/tableauserverclient/models/table_item.py +++ b/tableauserverclient/models/table_item.py @@ -17,6 +17,7 @@ def __init__(self, name, description=None): self._schema = None self._columns = None + self._data_quality_warnings = None @property def permissions(self): @@ -25,6 +26,13 @@ def permissions(self): raise UnpopulatedPropertyError(error) return self._permissions() + @property + def dqws(self): + if self._data_quality_warnings is None: + error = "Project item must be populated with dqws first." + raise UnpopulatedPropertyError(error) + return self._data_quality_warnings() + @property def id(self): return self._id @@ -86,6 +94,9 @@ def columns(self): def _set_columns(self, columns): self._columns = columns + def _set_data_quality_warnings(self, dqws): + self._data_quality_warnings = dqws + def _set_values(self, table_values): if "id" in table_values: self._id = table_values["id"] diff --git a/tableauserverclient/models/tableau_auth.py b/tableauserverclient/models/tableau_auth.py index 53aa33992..01787de4e 100644 --- a/tableauserverclient/models/tableau_auth.py +++ b/tableauserverclient/models/tableau_auth.py @@ -18,14 +18,20 @@ def __init__(self, username, password, site=None, site_id="", user_id_to_imperso def site(self): import warnings - warnings.warn("TableauAuth.site is deprecated, use TableauAuth.site_id instead.", DeprecationWarning) + warnings.warn( + "TableauAuth.site is deprecated, use TableauAuth.site_id instead.", + DeprecationWarning, + ) return self.site_id @site.setter def site(self, value): import warnings - warnings.warn("TableauAuth.site is deprecated, use TableauAuth.site_id instead.", DeprecationWarning) + warnings.warn( + "TableauAuth.site is deprecated, use TableauAuth.site_id instead.", + DeprecationWarning, + ) self.site_id = value @property diff --git a/tableauserverclient/models/task_item.py b/tableauserverclient/models/task_item.py index 26b0f348f..65709d5c9 100644 --- a/tableauserverclient/models/task_item.py +++ b/tableauserverclient/models/task_item.py @@ -10,7 +10,10 @@ class Type: DataAcceleration = "dataAcceleration" # This mapping is used to convert task type returned from server - _TASK_TYPE_MAPPING = {"RefreshExtractTask": Type.ExtractRefresh, "MaterializeViewsTask": Type.DataAcceleration} + _TASK_TYPE_MAPPING = { + "RefreshExtractTask": Type.ExtractRefresh, + "MaterializeViewsTask": Type.DataAcceleration, + } def __init__( self, @@ -78,7 +81,14 @@ def _parse_element(cls, element, ns): consecutive_failed_count = int(element.get("consecutiveFailedCount", 0)) id_ = element.get("id", None) return cls( - id_, task_type, priority, consecutive_failed_count, schedule_item.id, schedule_item, last_run_at, target + id_, + task_type, + priority, + consecutive_failed_count, + schedule_item.id, + schedule_item, + last_run_at, + target, ) @staticmethod diff --git a/tableauserverclient/models/user_item.py b/tableauserverclient/models/user_item.py index b9796cbae..65abf4cb6 100644 --- a/tableauserverclient/models/user_item.py +++ b/tableauserverclient/models/user_item.py @@ -1,6 +1,10 @@ import xml.etree.ElementTree as ET from .exceptions import UnpopulatedPropertyError -from .property_decorators import property_is_enum, property_not_empty, property_not_nullable +from .property_decorators import ( + property_is_enum, + property_not_empty, + property_not_nullable, +) from ..datetime_helpers import parse_datetime from .reference_item import ResourceReference @@ -128,12 +132,31 @@ def _parse_common_tags(self, user_xml, ns): if not isinstance(user_xml, ET.Element): user_xml = ET.fromstring(user_xml).find(".//t:user", namespaces=ns) if user_xml is not None: - (_, _, site_role, _, _, fullname, email, auth_setting, _) = self._parse_element(user_xml, ns) + ( + _, + _, + site_role, + _, + _, + fullname, + email, + auth_setting, + _, + ) = self._parse_element(user_xml, ns) self._set_values(None, None, site_role, None, None, fullname, email, auth_setting, None) return self def _set_values( - self, id, name, site_role, last_login, external_auth_user_id, fullname, email, auth_setting, domain_name + self, + id, + name, + site_role, + last_login, + external_auth_user_id, + fullname, + email, + auth_setting, + domain_name, ): if id is not None: self._id = id @@ -173,7 +196,15 @@ def from_response(cls, resp, ns): ) = cls._parse_element(user_xml, ns) user_item = cls(name, site_role) user_item._set_values( - id, name, site_role, last_login, external_auth_user_id, fullname, email, auth_setting, domain_name + id, + name, + site_role, + last_login, + external_auth_user_id, + fullname, + email, + auth_setting, + domain_name, ) all_user_items.append(user_item) return all_user_items @@ -198,7 +229,17 @@ def _parse_element(user_xml, ns): if domain_elem is not None: domain_name = domain_elem.get("name", None) - return id, name, site_role, last_login, external_auth_user_id, fullname, email, auth_setting, domain_name + return ( + id, + name, + site_role, + last_login, + external_auth_user_id, + fullname, + email, + auth_setting, + domain_name, + ) def __repr__(self): return "".format(self.id, self.name, self.site_role) diff --git a/tableauserverclient/models/workbook_item.py b/tableauserverclient/models/workbook_item.py index 20597364e..14ca8f33b 100644 --- a/tableauserverclient/models/workbook_item.py +++ b/tableauserverclient/models/workbook_item.py @@ -1,6 +1,10 @@ import xml.etree.ElementTree as ET from .exceptions import UnpopulatedPropertyError -from .property_decorators import property_not_nullable, property_is_boolean, property_is_data_acceleration_config +from .property_decorators import ( + property_not_nullable, + property_is_boolean, + property_is_data_acceleration_config, +) from .tag_item import TagItem from .view_item import ViewItem from .permissions_item import PermissionsRule diff --git a/tableauserverclient/server/__init__.py b/tableauserverclient/server/__init__.py index 314b477c2..c653a8966 100644 --- a/tableauserverclient/server/__init__.py +++ b/tableauserverclient/server/__init__.py @@ -1,5 +1,10 @@ from .request_factory import RequestFactory -from .request_options import CSVRequestOptions, ImageRequestOptions, PDFRequestOptions, RequestOptions +from .request_options import ( + CSVRequestOptions, + ImageRequestOptions, + PDFRequestOptions, + RequestOptions, +) from .filter import Filter from .sort import Sort from .. import ( @@ -7,6 +12,7 @@ DataAlertItem, DatasourceItem, DatabaseItem, + DQWItem, JobItem, BackgroundJobItem, GroupItem, diff --git a/tableauserverclient/server/endpoint/__init__.py b/tableauserverclient/server/endpoint/__init__.py index 5d55509cf..8653c0254 100644 --- a/tableauserverclient/server/endpoint/__init__.py +++ b/tableauserverclient/server/endpoint/__init__.py @@ -6,7 +6,11 @@ from .endpoint import Endpoint from .favorites_endpoint import Favorites from .flows_endpoint import Flows -from .exceptions import ServerResponseError, MissingRequiredFieldError, ServerInfoEndpointNotFoundError +from .exceptions import ( + ServerResponseError, + MissingRequiredFieldError, + ServerInfoEndpointNotFoundError, +) from .groups_endpoint import Groups from .jobs_endpoint import Jobs from .metadata_endpoint import Metadata diff --git a/tableauserverclient/server/endpoint/databases_endpoint.py b/tableauserverclient/server/endpoint/databases_endpoint.py index f9ff014d9..50826ee0b 100644 --- a/tableauserverclient/server/endpoint/databases_endpoint.py +++ b/tableauserverclient/server/endpoint/databases_endpoint.py @@ -2,6 +2,7 @@ from .exceptions import MissingRequiredFieldError from .permissions_endpoint import _PermissionsEndpoint from .default_permissions_endpoint import _DefaultPermissionsEndpoint +from .dqw_endpoint import _DataQualityWarningEndpoint from .. import RequestFactory, DatabaseItem, TableItem, PaginationItem, Permission @@ -16,6 +17,7 @@ def __init__(self, parent_srv): self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl) self._default_permissions = _DefaultPermissionsEndpoint(parent_srv, lambda: self.baseurl) + self._data_quality_warnings = _DataQualityWarningEndpoint(parent_srv, "database") @property def baseurl(self): @@ -116,3 +118,19 @@ def update_table_default_permissions(self, item): @api(version="3.5") def delete_table_default_permissions(self, item): self._default_permissions.delete_default_permissions(item, Permission.Resource.Table) + + @api(version="3.5") + def populate_dqw(self, item): + self._data_quality_warnings.populate(item) + + @api(version="3.5") + def update_dqw(self, item, warning): + return self._data_quality_warnings.update(item, warning) + + @api(version="3.5") + def add_dqw(self, item, warning): + return self._data_quality_warnings.add(item, warning) + + @api(version="3.5") + def delete_dqw(self, item): + self._data_quality_warnings.clear(item) diff --git a/tableauserverclient/server/endpoint/datasources_endpoint.py b/tableauserverclient/server/endpoint/datasources_endpoint.py index 6d117b2a0..ccdbfa0d1 100644 --- a/tableauserverclient/server/endpoint/datasources_endpoint.py +++ b/tableauserverclient/server/endpoint/datasources_endpoint.py @@ -1,11 +1,17 @@ from .endpoint import QuerysetEndpoint, api, parameter_added_in from .exceptions import InternalServerError, MissingRequiredFieldError from .permissions_endpoint import _PermissionsEndpoint +from .dqw_endpoint import _DataQualityWarningEndpoint from .fileuploads_endpoint import Fileuploads from .resource_tagger import _ResourceTagger from .. import RequestFactory, DatasourceItem, PaginationItem, ConnectionItem from ..query import QuerySet -from ...filesys_helpers import to_filename, make_download_path, get_file_type, get_file_object_size +from ...filesys_helpers import ( + to_filename, + make_download_path, + get_file_type, + get_file_object_size, +) from ...models.job_item import JobItem import os @@ -27,6 +33,7 @@ def __init__(self, parent_srv): super(Datasources, self).__init__(parent_srv) self._resource_tagger = _ResourceTagger(parent_srv) self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl) + self._data_quality_warnings = _DataQualityWarningEndpoint(self.parent_srv, "datasource") @property def baseurl(self): @@ -95,7 +102,10 @@ def download(self, datasource_id, filepath=None, include_extract=True, no_extrac if no_extract is False or no_extract is True: import warnings - warnings.warn("no_extract is deprecated, use include_extract instead.", DeprecationWarning) + warnings.warn( + "no_extract is deprecated, use include_extract instead.", + DeprecationWarning, + ) include_extract = not no_extract if not include_extract: @@ -174,7 +184,15 @@ def delete_extract(self, datasource_item): @api(version="2.0") @parameter_added_in(connections="2.8") @parameter_added_in(as_job="3.0") - def publish(self, datasource_item, file, mode, connection_credentials=None, connections=None, as_job=False): + def publish( + self, + datasource_item, + file, + mode, + connection_credentials=None, + connections=None, + as_job=False, + ): try: @@ -241,7 +259,11 @@ def publish(self, datasource_item, file, mode, connection_credentials=None, conn file_contents = file.read() xml_request, content_type = RequestFactory.Datasource.publish_req( - datasource_item, filename, file_contents, connection_credentials, connections + datasource_item, + filename, + file_contents, + connection_credentials, + connections, ) # Send the publishing request to server @@ -287,3 +309,19 @@ def update_permissions(self, item, permission_item): @api(version="2.0") def delete_permission(self, item, capability_item): self._permissions.delete(item, capability_item) + + @api(version="3.5") + def populate_dqw(self, item): + self._data_quality_warnings.populate(item) + + @api(version="3.5") + def update_dqw(self, item, warning): + return self._data_quality_warnings.update(item, warning) + + @api(version="3.5") + def add_dqw(self, item, warning): + return self._data_quality_warnings.add(item, warning) + + @api(version="3.5") + def delete_dqw(self, item): + self._data_quality_warnings.clear(item) diff --git a/tableauserverclient/server/endpoint/dqw_endpoint.py b/tableauserverclient/server/endpoint/dqw_endpoint.py new file mode 100644 index 000000000..e19ca7d90 --- /dev/null +++ b/tableauserverclient/server/endpoint/dqw_endpoint.py @@ -0,0 +1,61 @@ +import logging + +from .. import RequestFactory, DQWItem + +from .endpoint import Endpoint +from .exceptions import MissingRequiredFieldError + + +logger = logging.getLogger(__name__) + + +class _DataQualityWarningEndpoint(Endpoint): + def __init__(self, parent_srv, resource_type): + super(_DataQualityWarningEndpoint, self).__init__(parent_srv) + self.resource_type = resource_type + + @property + def baseurl(self): + return "{0}/sites/{1}/dataQualityWarnings/{2}".format( + self.parent_srv.baseurl, self.parent_srv.site_id, self.resource_type + ) + + def add(self, resource, warning): + url = "{baseurl}/{content_luid}".format(baseurl=self.baseurl, content_luid=resource.id) + add_req = RequestFactory.DQW.add_req(warning) + response = self.post_request(url, add_req) + warnings = DQWItem.from_response(response.content, self.parent_srv.namespace) + logger.info("Added dqw for resource {0}".format(resource.id)) + + return warnings + + def update(self, resource, warning): + url = "{baseurl}/{content_luid}".format(baseurl=self.baseurl, content_luid=resource.id) + add_req = RequestFactory.DQW.update_req(warning) + response = self.put_request(url, add_req) + warnings = DQWItem.from_response(response.content, self.parent_srv.namespace) + logger.info("Added dqw for resource {0}".format(resource.id)) + + return warnings + + def clear(self, resource): + url = "{baseurl}/{content_luid}".format(baseurl=self.baseurl, content_luid=resource.id) + return self.delete_request(url) + + def populate(self, item): + if not item.id: + error = "Server item is missing ID. Item must be retrieved from server first." + raise MissingRequiredFieldError(error) + + def dqw_fetcher(): + return self._get_data_quality_warnings(item) + + item._set_data_quality_warnings(dqw_fetcher) + logger.info("Populated permissions for item (ID: {0})".format(item.id)) + + def _get_data_quality_warnings(self, item, req_options=None): + url = "{baseurl}/{content_luid}".format(baseurl=self.baseurl, content_luid=item.id) + server_response = self.get_request(url, req_options) + dqws = DQWItem.from_response(server_response.content, self.parent_srv.namespace) + + return dqws diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index 92a20b21a..c7be8fc77 100644 --- a/tableauserverclient/server/endpoint/endpoint.py +++ b/tableauserverclient/server/endpoint/endpoint.py @@ -1,4 +1,9 @@ -from .exceptions import ServerResponseError, InternalServerError, NonXMLResponseError, EndpointUnavailableError +from .exceptions import ( + ServerResponseError, + InternalServerError, + NonXMLResponseError, + EndpointUnavailableError, +) from functools import wraps from xml.etree.ElementTree import ParseError from ..query import QuerySet @@ -39,7 +44,15 @@ def _safe_to_log(server_response): else: return server_response.content - def _make_request(self, method, url, content=None, auth_token=None, content_type=None, parameters=None): + def _make_request( + self, + method, + url, + content=None, + auth_token=None, + content_type=None, + parameters=None, + ): parameters = parameters or {} parameters.update(self.parent_srv.http_options) parameters["headers"] = Endpoint._make_common_headers(auth_token, content_type) @@ -95,7 +108,10 @@ def get_request(self, url, request_object=None, parameters=None): url = request_object.apply_query_params(url) return self._make_request( - self.parent_srv.session.get, url, auth_token=self.parent_srv.auth_token, parameters=parameters + self.parent_srv.session.get, + url, + auth_token=self.parent_srv.auth_token, + parameters=parameters, ) def delete_request(self, url): diff --git a/tableauserverclient/server/endpoint/flows_endpoint.py b/tableauserverclient/server/endpoint/flows_endpoint.py index 41cbe19cd..475166aad 100644 --- a/tableauserverclient/server/endpoint/flows_endpoint.py +++ b/tableauserverclient/server/endpoint/flows_endpoint.py @@ -1,6 +1,7 @@ from .endpoint import Endpoint, api from .exceptions import InternalServerError, MissingRequiredFieldError from .permissions_endpoint import _PermissionsEndpoint +from .dqw_endpoint import _DataQualityWarningEndpoint from .fileuploads_endpoint import Fileuploads from .resource_tagger import _ResourceTagger from .. import RequestFactory, FlowItem, PaginationItem, ConnectionItem @@ -26,6 +27,7 @@ def __init__(self, parent_srv): super(Flows, self).__init__(parent_srv) self._resource_tagger = _ResourceTagger(parent_srv) self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl) + self._data_quality_warnings = _DataQualityWarningEndpoint(self.parent_srv, "flow") @property def baseurl(self): @@ -214,3 +216,19 @@ def update_permissions(self, item, permission_item): @api(version="3.3") def delete_permission(self, item, capability_item): self._permissions.delete(item, capability_item) + + @api(version="3.5") + def populate_dqw(self, item): + self._data_quality_warnings.populate(item) + + @api(version="3.5") + def update_dqw(self, item, warning): + return self._data_quality_warnings.update(item, warning) + + @api(version="3.5") + def add_dqw(self, item, warning): + return self._data_quality_warnings.add(item, warning) + + @api(version="3.5") + def delete_dqw(self, item): + self._data_quality_warnings.clear(item) diff --git a/tableauserverclient/server/endpoint/groups_endpoint.py b/tableauserverclient/server/endpoint/groups_endpoint.py index 4a09872cb..b771e56d8 100644 --- a/tableauserverclient/server/endpoint/groups_endpoint.py +++ b/tableauserverclient/server/endpoint/groups_endpoint.py @@ -33,7 +33,10 @@ def populate_users(self, group_item, req_options=None): # Define an inner function that we bind to the model_item's `.user` property. def user_pager(): - return Pager(lambda options: self._get_users_for_group(group_item, options), req_options) + return Pager( + lambda options: self._get_users_for_group(group_item, options), + req_options, + ) group_item._set_users(user_pager) diff --git a/tableauserverclient/server/endpoint/permissions_endpoint.py b/tableauserverclient/server/endpoint/permissions_endpoint.py index 0992f5ca9..7035837f4 100644 --- a/tableauserverclient/server/endpoint/permissions_endpoint.py +++ b/tableauserverclient/server/endpoint/permissions_endpoint.py @@ -46,7 +46,12 @@ def delete(self, resource, rules): for capability, mode in rule.capabilities.items(): " /permissions/groups/group-id/capability-name/capability-mode" url = "{0}/{1}/permissions/{2}/{3}/{4}/{5}".format( - self.owner_baseurl(), resource.id, rule.grantee.tag_name + "s", rule.grantee.id, capability, mode + self.owner_baseurl(), + resource.id, + rule.grantee.tag_name + "s", + rule.grantee.id, + capability, + mode, ) logger.debug("Removing {0} permission for capabilty {1}".format(mode, capability)) diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index 3a5e665fa..d582dca26 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -65,7 +65,13 @@ def create(self, schedule_item): return new_schedule @api(version="2.8") - def add_to_schedule(self, schedule_id, workbook=None, datasource=None, task_type=TaskItem.Type.ExtractRefresh): + def add_to_schedule( + self, + schedule_id, + workbook=None, + datasource=None, + task_type=TaskItem.Type.ExtractRefresh, + ): def add_to(resource, type_, req_factory): id_ = resource.id url = "{0}/{1}/{2}s".format(self.siteurl, schedule_id, type_) @@ -79,7 +85,12 @@ def add_to(resource, type_, req_factory): logger.info("Added {} to {} to schedule {}".format(type_, id_, schedule_id)) if error is not None or warnings is not None: - return AddResponse(result=False, error=error, warnings=warnings, task_created=task_created) + return AddResponse( + result=False, + error=error, + warnings=warnings, + task_created=task_created, + ) else: return OK diff --git a/tableauserverclient/server/endpoint/server_info_endpoint.py b/tableauserverclient/server/endpoint/server_info_endpoint.py index 98d996b52..8776477d3 100644 --- a/tableauserverclient/server/endpoint/server_info_endpoint.py +++ b/tableauserverclient/server/endpoint/server_info_endpoint.py @@ -1,5 +1,9 @@ from .endpoint import Endpoint, api -from .exceptions import ServerResponseError, ServerInfoEndpointNotFoundError, EndpointUnavailableError +from .exceptions import ( + ServerResponseError, + ServerInfoEndpointNotFoundError, + EndpointUnavailableError, +) from ...models import ServerInfoItem import logging diff --git a/tableauserverclient/server/endpoint/tables_endpoint.py b/tableauserverclient/server/endpoint/tables_endpoint.py index e35535d19..ac53484db 100644 --- a/tableauserverclient/server/endpoint/tables_endpoint.py +++ b/tableauserverclient/server/endpoint/tables_endpoint.py @@ -1,6 +1,7 @@ from .endpoint import api, Endpoint from .exceptions import MissingRequiredFieldError from .permissions_endpoint import _PermissionsEndpoint +from .dqw_endpoint import _DataQualityWarningEndpoint from ..pager import Pager from .. import RequestFactory, TableItem, ColumnItem, PaginationItem @@ -15,6 +16,7 @@ def __init__(self, parent_srv): super(Tables, self).__init__(parent_srv) self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl) + self._data_quality_warnings = _DataQualityWarningEndpoint(self.parent_srv, "table") @property def baseurl(self): @@ -70,7 +72,10 @@ def populate_columns(self, table_item, req_options=None): raise MissingRequiredFieldError(error) def column_fetcher(): - return Pager(lambda options: self._get_columns_for_table(table_item, options), req_options) + return Pager( + lambda options: self._get_columns_for_table(table_item, options), + req_options, + ) table_item._set_columns(column_fetcher) logger.info("Populated columns for table (ID: {0}".format(table_item.id)) @@ -113,3 +118,19 @@ def update_permissions(self, item, rules): @api(version="3.5") def delete_permission(self, item, rules): return self._permissions.delete(item, rules) + + @api(version="3.5") + def populate_dqw(self, item): + self._data_quality_warnings.populate(item) + + @api(version="3.5") + def update_dqw(self, item, warning): + return self._data_quality_warnings.update(item, warning) + + @api(version="3.5") + def add_dqw(self, item, warning): + return self._data_quality_warnings.add(item, warning) + + @api(version="3.5") + def delete_dqw(self, item): + self._data_quality_warnings.clear(item) diff --git a/tableauserverclient/server/endpoint/tasks_endpoint.py b/tableauserverclient/server/endpoint/tasks_endpoint.py index abc249721..aaa5069c3 100644 --- a/tableauserverclient/server/endpoint/tasks_endpoint.py +++ b/tableauserverclient/server/endpoint/tasks_endpoint.py @@ -42,7 +42,11 @@ def get_by_id(self, task_id): error = "No Task ID provided" raise ValueError(error) logger.info("Querying a single task by id ({})".format(task_id)) - url = "{}/{}/{}".format(self.baseurl, self.__normalize_task_type(TaskItem.Type.ExtractRefresh), task_id) + url = "{}/{}/{}".format( + self.baseurl, + self.__normalize_task_type(TaskItem.Type.ExtractRefresh), + task_id, + ) server_response = self.get_request(url) return TaskItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -53,7 +57,9 @@ def run(self, task_item): raise MissingRequiredFieldError(error) url = "{0}/{1}/{2}/runNow".format( - self.baseurl, self.__normalize_task_type(TaskItem.Type.ExtractRefresh), task_item.id + self.baseurl, + self.__normalize_task_type(TaskItem.Type.ExtractRefresh), + task_item.id, ) run_req = RequestFactory.Task.run_req(task_item) server_response = self.post_request(url, run_req) diff --git a/tableauserverclient/server/endpoint/users_endpoint.py b/tableauserverclient/server/endpoint/users_endpoint.py index 3318e6bb3..6adbf92fb 100644 --- a/tableauserverclient/server/endpoint/users_endpoint.py +++ b/tableauserverclient/server/endpoint/users_endpoint.py @@ -1,6 +1,13 @@ from .endpoint import QuerysetEndpoint, api from .exceptions import MissingRequiredFieldError -from .. import RequestFactory, RequestOptions, UserItem, WorkbookItem, PaginationItem, GroupItem +from .. import ( + RequestFactory, + RequestOptions, + UserItem, + WorkbookItem, + PaginationItem, + GroupItem, +) from ..pager import Pager import copy @@ -105,7 +112,10 @@ def populate_groups(self, user_item, req_options=None): raise MissingRequiredFieldError(error) def groups_for_user_pager(): - return Pager(lambda options: self._get_groups_for_user(user_item, options), req_options) + return Pager( + lambda options: self._get_groups_for_user(user_item, options), + req_options, + ) user_item._set_groups(groups_for_user_pager) diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py index aa72979dd..df14674c6 100644 --- a/tableauserverclient/server/endpoint/workbooks_endpoint.py +++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py @@ -5,7 +5,12 @@ from .resource_tagger import _ResourceTagger from .. import RequestFactory, WorkbookItem, ConnectionItem, ViewItem, PaginationItem from ...models.job_item import JobItem -from ...filesys_helpers import to_filename, make_download_path, get_file_type, get_file_object_size +from ...filesys_helpers import ( + to_filename, + make_download_path, + get_file_type, + get_file_object_size, +) import os import logging @@ -140,7 +145,10 @@ def download(self, workbook_id, filepath=None, include_extract=True, no_extract= if no_extract is False or no_extract is True: import warnings - warnings.warn("no_extract is deprecated, use include_extract instead.", DeprecationWarning) + warnings.warn( + "no_extract is deprecated, use include_extract instead.", + DeprecationWarning, + ) include_extract = not no_extract if not include_extract: @@ -176,7 +184,11 @@ def _get_views_for_workbook(self, workbook_item, usage): if usage: url += "?includeUsageStatistics=true" server_response = self.get_request(url) - views = ViewItem.from_response(server_response.content, self.parent_srv.namespace, workbook_id=workbook_item.id) + views = ViewItem.from_response( + server_response.content, + self.parent_srv.namespace, + workbook_id=workbook_item.id, + ) return views # Get all connections of workbook @@ -267,7 +279,10 @@ def publish( if connection_credentials is not None: import warnings - warnings.warn("connection_credentials is being deprecated. Use connections instead", DeprecationWarning) + warnings.warn( + "connection_credentials is being deprecated. Use connections instead", + DeprecationWarning, + ) try: # Expect file to be a filepath @@ -333,7 +348,10 @@ def publish( url = "{0}&uploadSessionId={1}".format(url, upload_session_id) conn_creds = connection_credentials xml_request, content_type = RequestFactory.Workbook.publish_req_chunked( - workbook_item, connection_credentials=conn_creds, connections=connections, hidden_views=hidden_views + workbook_item, + connection_credentials=conn_creds, + connections=connections, + hidden_views=hidden_views, ) else: logger.info("Publishing {0} to server".format(filename)) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index d2e921479..c03a4fadc 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -177,7 +177,14 @@ def update_req(self, datasource_item): return ET.tostring(xml_request) - def publish_req(self, datasource_item, filename, file_contents, connection_credentials=None, connections=None): + def publish_req( + self, + datasource_item, + filename, + file_contents, + connection_credentials=None, + connections=None, + ): xml_request = self._generate_xml(datasource_item, connection_credentials, connections) parts = { @@ -193,6 +200,66 @@ def publish_req_chunked(self, datasource_item, connection_credentials=None, conn return _add_multipart(parts) +class DQWRequest(object): + def add_req(self, dqw_item): + xml_request = ET.Element("tsRequest") + dqw_element = ET.SubElement(xml_request, "dataQualityWarning") + + dqw_element.attrib["isActive"] = str(dqw_item.active).lower() + dqw_element.attrib["isSevere"] = str(dqw_item.severe).lower() + + dqw_element.attrib["type"] = dqw_item.warning_type + + if dqw_item.message: + dqw_element.attrib["message"] = str(dqw_item.message) + + return ET.tostring(xml_request) + + def update_req(self, database_item): + xml_request = ET.Element("tsRequest") + dqw_element = ET.SubElement(xml_request, "dataQualityWarning") + + dqw_element.attrib["isActive"] = str(dqw_item.active).lower() + dqw_element.attrib["isSevere"] = str(dqw_item.severe).lower() + + dqw_element.attrib["type"] = dqw_item.warning_type + + if dqw_item.message: + dqw_element.attrib["message"] = str(dqw_item.message) + + return ET.tostring(xml_request) + + +class DQWRequest(object): + def add_req(self, dqw_item): + xml_request = ET.Element("tsRequest") + dqw_element = ET.SubElement(xml_request, "dataQualityWarning") + + dqw_element.attrib["isActive"] = str(dqw_item.active).lower() + dqw_element.attrib["isSevere"] = str(dqw_item.severe).lower() + + dqw_element.attrib["type"] = dqw_item.warning_type + + if dqw_item.message: + dqw_element.attrib["message"] = str(dqw_item.message) + + return ET.tostring(xml_request) + + def update_req(self, database_item): + xml_request = ET.Element("tsRequest") + dqw_element = ET.SubElement(xml_request, "dataQualityWarning") + + dqw_element.attrib["isActive"] = str(dqw_item.active).lower() + dqw_element.attrib["isSevere"] = str(dqw_item.severe).lower() + + dqw_element.attrib["type"] = dqw_item.warning_type + + if dqw_item.message: + dqw_element.attrib["message"] = str(dqw_item.message) + + return ET.tostring(xml_request) + + class FavoriteRequest(object): def _add_to_req(self, id_, target_type, label): """ @@ -223,7 +290,10 @@ def add_workbook_req(self, id_, name): class FileuploadRequest(object): def chunk_req(self, chunk): - parts = {"request_payload": ("", "", "text/xml"), "tableau_file": ("file", chunk, "application/octet-stream")} + parts = { + "request_payload": ("", "", "text/xml"), + "tableau_file": ("file", chunk, "application/octet-stream"), + } return _add_multipart(parts) @@ -724,7 +794,13 @@ def add_req(self, user_item): class WorkbookRequest(object): - def _generate_xml(self, workbook_item, connection_credentials=None, connections=None, hidden_views=None): + def _generate_xml( + self, + workbook_item, + connection_credentials=None, + connections=None, + hidden_views=None, + ): xml_request = ET.Element("tsRequest") workbook_element = ET.SubElement(xml_request, "workbook") workbook_element.attrib["name"] = workbook_item.name @@ -778,7 +854,13 @@ def update_req(self, workbook_item): return ET.tostring(xml_request) def publish_req( - self, workbook_item, filename, file_contents, connection_credentials=None, connections=None, hidden_views=None + self, + workbook_item, + filename, + file_contents, + connection_credentials=None, + connections=None, + hidden_views=None, ): xml_request = self._generate_xml( workbook_item, @@ -793,7 +875,13 @@ def publish_req( } return _add_multipart(parts) - def publish_req_chunked(self, workbook_item, connection_credentials=None, connections=None, hidden_views=None): + def publish_req_chunked( + self, + workbook_item, + connection_credentials=None, + connections=None, + hidden_views=None, + ): xml_request = self._generate_xml( workbook_item, connection_credentials=connection_credentials, @@ -932,6 +1020,7 @@ class RequestFactory(object): DataAlert = DataAlertRequest() Datasource = DatasourceRequest() Database = DatabaseRequest() + DQW = DQWRequest() Empty = EmptyRequest() Favorite = FavoriteRequest() Fileupload = FileuploadRequest() diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index b45098e8a..057c98877 100644 --- a/tableauserverclient/server/server.py +++ b/tableauserverclient/server/server.py @@ -25,7 +25,10 @@ Favorites, DataAlerts, ) -from .endpoint.exceptions import EndpointUnavailableError, ServerInfoEndpointNotFoundError +from .endpoint.exceptions import ( + EndpointUnavailableError, + ServerInfoEndpointNotFoundError, +) import requests @@ -34,7 +37,13 @@ except ImportError: from distutils.version import LooseVersion as Version -_PRODUCT_TO_REST_VERSION = {"10.0": "2.3", "9.3": "2.2", "9.2": "2.1", "9.1": "2.0", "9.0": "2.0"} +_PRODUCT_TO_REST_VERSION = { + "10.0": "2.3", + "9.3": "2.2", + "9.2": "2.1", + "9.1": "2.0", + "9.0": "2.0", +} class Server(object): diff --git a/test/assets/dqw_by_content_type.xml b/test/assets/dqw_by_content_type.xml new file mode 100644 index 000000000..c65deb6d9 --- /dev/null +++ b/test/assets/dqw_by_content_type.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/test/test_database.py b/test/test_database.py index fb9ffbd86..4de623dae 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -11,12 +11,12 @@ GET_XML = 'database_get.xml' POPULATE_PERMISSIONS_XML = 'database_populate_permissions.xml' UPDATE_XML = 'database_update.xml' +GET_DQW_BY_CONTENT = "dqw_by_content_type.xml" class DatabaseTests(unittest.TestCase): def setUp(self): self.server = TSC.Server('http://test') - # Fake signin self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67' self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM' @@ -81,6 +81,27 @@ def test_populate_permissions(self): TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow, }) + def test_populate_data_quality_warning(self): + with open(asset(GET_DQW_BY_CONTENT), 'rb') as f: + response_xml = f.read().decode('utf-8') + with requests_mock.mock() as m: + m.get(self.server.databases._data_quality_warnings.baseurl + '/94441d26-9a52-4a42-b0fb-3f94792d1aac', text=response_xml) + single_database = TSC.DatabaseItem('test') + single_database._id = '94441d26-9a52-4a42-b0fb-3f94792d1aac' + + self.server.databases.populate_dqw(single_database) + dqws = single_database.dqws + first_dqw = dqws.pop() + self.assertEqual(first_dqw.id, "c2e0e406-84fb-4f4e-9998-f20dd9306710") + self.assertEqual(first_dqw.warning_type, "WARNING") + self.assertEqual(first_dqw.message, "Hello, World!") + self.assertEqual(first_dqw.owner_id, "eddc8c5f-6af0-40be-b6b0-2c790290a43f") + self.assertEqual(first_dqw.active, True) + self.assertEqual(first_dqw.severe, True) + self.assertEqual(str(first_dqw.created_at), "2021-04-09 18:39:54+00:00") + self.assertEqual(str(first_dqw.updated_at), "2021-04-09 18:39:54+00:00") + + def test_delete(self): with requests_mock.mock() as m: m.delete(self.baseurl + '/0448d2ed-590d-4fa0-b272-a2a8a24555b5', status_code=204)