8000 Data Quality Warning Support (#836) · TableauJoe/server-client-python@3769e0d · GitHub
[go: up one dir, main page]

Skip to content

Commit 3769e0d

Browse files
authored
Data Quality Warning Support (tableau#836)
Add support for DQWs by endpoint following the pattern used by permissions and tags -- leave by ID and Triggers dqw urls for another PR
1 parent cec0fa6 commit 3769e0d

39 files changed

+852
-67
lines changed

contributing.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,9 @@ python setup.py test
6767
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.
6868

6969
```shell
70-
pycodestyle tableauserverclient test samples
70+
# this will run the formatter without making changes
71+
black --line-length 120 tableauserverclient --check
72+
73+
# this will format the directory and code for you
74+
black --line-length 120 tableauserverclient
7175
```

tableauserverclient/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
ConnectionItem,
55
DataAlertItem,
66
DatasourceItem,
7+
DQWItem,
78
GroupItem,
89
JobItem,
910
BackgroundJobItem,

tableauserverclient/_version.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=
7777
dispcmd = str([c] + args)
7878
# remember shell=False, so use git.cmd on windows, not just git
7979
p = subprocess.Popen(
80-
[c] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None)
80+
[c] + args,
81+
cwd=cwd,
82+
env=env,
83+
stdout=subprocess.PIPE,
84+
stderr=(subprocess.PIPE if hide_stderr else None),
8185
)
8286
break
8387
except EnvironmentError:
@@ -243,7 +247,17 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
243247
# if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
244248
# if there isn't one, this yields HEX[-dirty] (no NUM)
245249
describe_out, rc = run_command(
246-
GITS, ["describe", "--tags", "--dirty", "--always", "--long", "--match", "%s*" % tag_prefix], cwd=root
250+
GITS,
251+
[
252+
"describe",
253+
"--tags",
254+
"--dirty",
255+
"--always",
256+
"--long",
257+
"--match",
258+
"%s*" % tag_prefix,
259+
],
260+
cwd=root,
247261
)
248262
# --long was added in git-1.5.5
249263
if describe_out is None:
@@ -285,7 +299,10 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
285299
if verbose:
286300
fmt = "tag '%s' doesn't start with prefix '%s'"
287301
print(fmt % (full_tag, tag_prefix))
288-
pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % (full_tag, tag_prefix)
302+
pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % (
303+
full_tag,
304+
tag_prefix,
305+
)
289306
return pieces
290307
pieces["closest-tag"] = full_tag[len(tag_prefix) :]
291308

tableauserverclient/models/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,18 @@
55
from .data_alert_item import DataAlertItem
66
from .datasource_item import DatasourceItem
77
from .database_item import DatabaseItem
8+
from .dqw_item import DQWItem
89
from .exceptions import UnpopulatedPropertyError
910
from .favorites_item import FavoriteItem
1011
from .group_item import GroupItem
1112
from .flow_item import FlowItem
12-
from .interval_item import IntervalItem, DailyInterval, WeeklyInterval, MonthlyInterval, HourlyInterval
13+
from .interval_item import (
14+
IntervalItem,
15+
DailyInterval,
16+
WeeklyInterval,
17+
MonthlyInterval,
18+
HourlyInterval,
19+
)
1320
from .job_item import JobItem, BackgroundJobItem
1421
from .pagination_item import PaginationItem
1522
from .project_item import ProjectItem

tableauserverclient/models/data_alert_item.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import xml.etree.ElementTree as ET
22

3-
from .property_decorators import property_not_empty, property_is_enum, property_is_boolean
3+
from .property_decorators import (
4+
property_not_empty,
5+
property_is_enum,
6+
property_is_boolean,
7+
)
48
from .user_item import UserItem
59
from .view_item import ViewItem
610

tableauserverclient/models/database_item.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import xml.etree.ElementTree as ET
22

3-
from .property_decorators import property_is_enum, property_not_empty, property_is_boolean
3+
from .property_decorators import (
4+
property_is_enum,
5+
property_not_empty,
6+
property_is_boolean,
7+
)
48
from .exceptions import UnpopulatedPropertyError
59

610

@@ -34,8 +38,17 @@ def __init__(self, name, description=None, content_permissions=None):
3438
self._permissions = None
3539
self._default_table_permissions = None
3640

41+
self._data_quality_warnings = None
42+
3743
self._tables = None # Not implemented yet
3844

45+
@property
46+
def dqws(self):
47+
if self._data_quality_warnings is None:
48+
error = "Project item must be populated with permissions first."
49+
raise UnpopulatedPropertyError(error)
50+
return self._data_quality_warnings()
51+
3952
@property
4053
def content_permissions(self):
4154
return self._content_permissions
@@ -229,7 +242,14 @@ def _set_tables(self, tables):
229242
self._tables = tables
230243

231244
def _set_default_permissions(self, permissions, content_type):
232-
setattr(self, "_default_{content}_permissions".format(content=content_type), permissions)
245+
setattr(
246+
self,
247+
"_default_{content}_permissions".format(content=content_type),
248+
permissions,
249+
)
250+
251+
def _set_data_quality_warnings(self, dqw):
252+
self._data_quality_warnings = dqw
233253

234254
@classmethod
235255
def from_response(cls, resp, ns):

tableauserverclient/models/datasource_item.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import xml.etree.ElementTree as ET
22
from .exceptions import UnpopulatedPropertyError
3-
from .property_decorators import property_not_nullable, property_is_boolean, property_is_enum
3+
from .property_decorators import (
4+
property_not_nullable,
5+
property_is_boolean,
6+
property_is_enum,
7+
)
48
from .tag_item import TagItem
59
from ..datetime_helpers import parse_datetime
610
import copy
@@ -35,6 +39,7 @@ def __init__(self, project_id, name=None):
3539
self.tags = set()
3640

3741
self._permissions = None
42+
self._data_quality_warnings = None
3843

3944
@property
4045
def ask_data_enablement(self):
@@ -94,6 +99,13 @@ def encrypt_extracts(self):
9499
def encrypt_extracts(self, value):
95100
self._encrypt_extracts = value
96101

102+
@property
103+
def dqws(self):
104+
if self._data_quality_warnings is None:
105+
error = "Project item must be populated with dqws first."
106+
raise UnpopulatedPropertyError(error)
107+
return self._data_quality_warnings()
108+
97109
@property
98110
def has_extracts(self):
99111
return self._has_extracts
@@ -142,6 +154,9 @@ def _set_connections(self, connections):
142154
def _set_permissions(self, permissions):
143155
self._permissions = permissions
144156

157+
def _set_data_quality_warnings(self, dqws):
158+
self._data_quality_warnings = dqws
159+
145160
def _parse_common_elements(self, datasource_xml, ns):
146161
if not isinstance(datasource_xml, ET.Element):
147162
datasource_xml = ET.fromstring(datasource_xml).find(".//t:datasource", namespaces=ns)
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import xml.etree.ElementTree as ET
2+
from ..datetime_helpers import parse_datetime
3+
4+
5+
class DQWItem(object):
6+
class WarningType:
7+
WARNING = "WARNING"
8+
DEPRECATED = "DEPRECATED"
9+
STALE = "STALE"
10+
SENSITIVE_DATA = "SENSITIVE_DATA"
11+
MAINTENANCE = "MAINTENANCE"
12+
13+
def __init__(self, warning_type="WARNING", message=None, active=True, severe=False):
14+
self._id = None
15+
# content related
16+
self._content_id = None
17+
self._content_type = None
18+
19+
# DQW related
20+
self.warning_type = warning_type
21+
self.message = message
22+
self.active = active
23+
self.severe = severe
24+
self._created_at = None
25+
self._updated_at = None
26+
27+
# owner
28+
self._owner_display_name = None
29+
self._owner_id = None
30+
31+
@property
32+
def id(self):
33+
return self._id
34+
35+
@property
36+
def content_id(self):
37+
return self._content_id
38+
39+
@property
40+
def content_type(self):
41+
return self._content_type
42+
43+
@property
44+
def owner_display_name(self):
45+
return self._owner_display_name
46+
47+
@property
48+
def owner_id(self):
49+
return self._owner_id
50+
51+
@property
52+
def warning_type(self):
53+
return self._warning_type
54+
55+
@warning_type.setter
56+
def warning_type(self, value):
57+
self._warning_type = value
58+
59+
@property
60+
def message(self):
61+
return self._message
62+
63+
@message.setter
64+
def message(self, value):
65+
self._message = value
66+
67+
@property
68+
def active(self):
69+
return self._active
70+
71+
@active.setter
72+
def active(self, value):
73+
self._active = value
74+
75+
@property
76+
def severe(self):
77+
return self._severe
78+
79+
@severe.setter
80+
def severe(self, value):
81+
self._severe = value
82+
83+
@property
84+
def active(self):
85+
return self._active
86+
87+
@active.setter
88+
def active(self, value):
89+
self._active = value
90+
91+
@property
92+
def created_at(self):
93+
return self._created_at
94+
95+
@created_at.setter
96+
def created_at(self, value):
97+
self._created_at = value
98+
99+
@property
100+
def updated_at(self):
101+
return self._updated_at
102+
103+
@updated_at.setter
104+
def updated_at(self, value):
105+
self._updated_at = value
106+
107+
@classmethod
108+
def from_response(cls, resp, ns):
109+
return cls.from_xml_element(ET.fromstring(resp), ns)
110+
111+
@classmethod
112+
def from_xml_element(cls, parsed_response, ns):
113+
all_dqws = []
114+
dqw_elem_list = parsed_response.findall(".//t:dataQualityWarning", namespaces=ns)
115+
for dqw_elem in dqw_elem_list:
116+
dqw = DQWItem()
117+
dqw._id = dqw_elem.get("id", None)
118+
dqw._owner_display_name = dqw_elem.get("userDisplayName", None)
119+
dqw._content_id = dqw_elem.get("contentId", None)
120+
dqw._content_type = dqw_elem.get("contentType", None)
121+
dqw.message = dqw_elem.get("message", None)
122+
dqw.warning_type = dqw_elem.get("type", None)
123+
124+
is_active = dqw_elem.get("isActive", None)
125+
if is_active is not None:
126+
dqw._active = string_to_bool(is_active)
127+
128+
is_severe = dqw_elem.get("isSevere", None)
129+
if is_severe is not None:
130+
dqw._severe = string_to_bool(is_severe)
131+
132+
dqw._created_at = parse_datetime(dqw_elem.get("createdAt", None))
133+
dqw._updated_at = parse_datetime(dqw_elem.get("updatedAt", None))
134+
135+
owner_id = None
136+
owner_tag = dqw_elem.find(".//t:owner", namespaces=ns)
137+
if owner_tag is not None:
138+
owner_id = owner_tag.get("id", None)
139+
dqw._owner_id = owner_id
140+
141+
all_dqws.append(dqw)
142+
143+
return all_dqws
144+
145+
146+
# Used to convert string represented boolean to a boolean type
147+
def string_to_bool(s):
148+
return s.lower() == "true"

0 commit comments

Comments
 (0)
0