tableauserverclient 0.32__py3-none-any.whl → 0.34__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tableauserverclient/__init__.py +34 -18
- tableauserverclient/_version.py +3 -3
- tableauserverclient/config.py +20 -6
- tableauserverclient/models/__init__.py +12 -0
- tableauserverclient/models/column_item.py +1 -1
- tableauserverclient/models/connection_credentials.py +1 -1
- tableauserverclient/models/connection_item.py +10 -8
- tableauserverclient/models/custom_view_item.py +29 -6
- tableauserverclient/models/data_acceleration_report_item.py +2 -2
- tableauserverclient/models/data_alert_item.py +5 -5
- tableauserverclient/models/data_freshness_policy_item.py +6 -6
- tableauserverclient/models/database_item.py +8 -2
- tableauserverclient/models/datasource_item.py +10 -10
- tableauserverclient/models/dqw_item.py +1 -1
- tableauserverclient/models/favorites_item.py +5 -6
- tableauserverclient/models/fileupload_item.py +1 -1
- tableauserverclient/models/flow_item.py +12 -12
- tableauserverclient/models/flow_run_item.py +3 -3
- tableauserverclient/models/group_item.py +4 -4
- tableauserverclient/models/groupset_item.py +53 -0
- tableauserverclient/models/interval_item.py +36 -23
- tableauserverclient/models/job_item.py +26 -10
- tableauserverclient/models/linked_tasks_item.py +102 -0
- tableauserverclient/models/metric_item.py +5 -5
- tableauserverclient/models/pagination_item.py +1 -1
- tableauserverclient/models/permissions_item.py +19 -14
- tableauserverclient/models/project_item.py +35 -19
- tableauserverclient/models/property_decorators.py +12 -11
- tableauserverclient/models/reference_item.py +2 -2
- tableauserverclient/models/revision_item.py +3 -3
- tableauserverclient/models/schedule_item.py +2 -2
- tableauserverclient/models/server_info_item.py +26 -6
- tableauserverclient/models/site_item.py +69 -3
- tableauserverclient/models/subscription_item.py +3 -3
- tableauserverclient/models/table_item.py +1 -1
- tableauserverclient/models/tableau_auth.py +115 -5
- tableauserverclient/models/tableau_types.py +11 -9
- tableauserverclient/models/tag_item.py +3 -4
- tableauserverclient/models/task_item.py +4 -4
- tableauserverclient/models/user_item.py +47 -17
- tableauserverclient/models/view_item.py +11 -10
- tableauserverclient/models/virtual_connection_item.py +78 -0
- tableauserverclient/models/webhook_item.py +6 -6
- tableauserverclient/models/workbook_item.py +90 -12
- tableauserverclient/namespace.py +1 -1
- tableauserverclient/server/__init__.py +2 -1
- tableauserverclient/server/endpoint/__init__.py +8 -0
- tableauserverclient/server/endpoint/auth_endpoint.py +68 -11
- tableauserverclient/server/endpoint/custom_views_endpoint.py +124 -19
- tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py +2 -2
- tableauserverclient/server/endpoint/data_alert_endpoint.py +14 -14
- tableauserverclient/server/endpoint/databases_endpoint.py +32 -17
- tableauserverclient/server/endpoint/datasources_endpoint.py +150 -59
- tableauserverclient/server/endpoint/default_permissions_endpoint.py +19 -18
- tableauserverclient/server/endpoint/dqw_endpoint.py +9 -9
- tableauserverclient/server/endpoint/endpoint.py +47 -31
- tableauserverclient/server/endpoint/exceptions.py +23 -7
- tableauserverclient/server/endpoint/favorites_endpoint.py +31 -31
- tableauserverclient/server/endpoint/fileuploads_endpoint.py +11 -13
- tableauserverclient/server/endpoint/flow_runs_endpoint.py +59 -17
- tableauserverclient/server/endpoint/flow_task_endpoint.py +2 -2
- tableauserverclient/server/endpoint/flows_endpoint.py +73 -35
- tableauserverclient/server/endpoint/groups_endpoint.py +96 -27
- tableauserverclient/server/endpoint/groupsets_endpoint.py +127 -0
- tableauserverclient/server/endpoint/jobs_endpoint.py +79 -12
- tableauserverclient/server/endpoint/linked_tasks_endpoint.py +45 -0
- tableauserverclient/server/endpoint/metadata_endpoint.py +2 -2
- tableauserverclient/server/endpoint/metrics_endpoint.py +10 -10
- tableauserverclient/server/endpoint/permissions_endpoint.py +13 -15
- tableauserverclient/server/endpoint/projects_endpoint.py +124 -30
- tableauserverclient/server/endpoint/resource_tagger.py +139 -6
- tableauserverclient/server/endpoint/schedules_endpoint.py +17 -18
- tableauserverclient/server/endpoint/server_info_endpoint.py +40 -5
- tableauserverclient/server/endpoint/sites_endpoint.py +282 -17
- tableauserverclient/server/endpoint/subscriptions_endpoint.py +10 -10
- tableauserverclient/server/endpoint/tables_endpoint.py +33 -19
- tableauserverclient/server/endpoint/tasks_endpoint.py +8 -8
- tableauserverclient/server/endpoint/users_endpoint.py +405 -19
- tableauserverclient/server/endpoint/views_endpoint.py +111 -25
- tableauserverclient/server/endpoint/virtual_connections_endpoint.py +174 -0
- tableauserverclient/server/endpoint/webhooks_endpoint.py +11 -11
- tableauserverclient/server/endpoint/workbooks_endpoint.py +735 -68
- tableauserverclient/server/filter.py +2 -2
- tableauserverclient/server/pager.py +8 -10
- tableauserverclient/server/query.py +70 -20
- tableauserverclient/server/request_factory.py +213 -41
- tableauserverclient/server/request_options.py +125 -145
- tableauserverclient/server/server.py +73 -9
- tableauserverclient/server/sort.py +2 -2
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/METADATA +17 -17
- tableauserverclient-0.34.dist-info/RECORD +106 -0
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/WHEEL +1 -1
- tableauserverclient-0.32.dist-info/RECORD +0 -100
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/LICENSE +0 -0
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/LICENSE.versioneer +0 -0
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/top_level.txt +0 -0
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
import datetime
|
|
3
3
|
import xml.etree.ElementTree as ET
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Optional
|
|
5
5
|
|
|
6
6
|
from defusedxml.ElementTree import fromstring
|
|
7
7
|
|
|
8
8
|
from tableauserverclient.datetime_helpers import parse_datetime
|
|
9
|
-
from .connection_item import ConnectionItem
|
|
10
|
-
from .dqw_item import DQWItem
|
|
11
|
-
from .exceptions import UnpopulatedPropertyError
|
|
12
|
-
from .permissions_item import Permission
|
|
13
|
-
from .property_decorators import property_not_nullable
|
|
14
|
-
from .tag_item import TagItem
|
|
9
|
+
from tableauserverclient.models.connection_item import ConnectionItem
|
|
10
|
+
from tableauserverclient.models.dqw_item import DQWItem
|
|
11
|
+
from tableauserverclient.models.exceptions import UnpopulatedPropertyError
|
|
12
|
+
from tableauserverclient.models.permissions_item import Permission
|
|
13
|
+
from tableauserverclient.models.property_decorators import property_not_nullable
|
|
14
|
+
from tableauserverclient.models.tag_item import TagItem
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
class FlowItem
|
|
17
|
+
class FlowItem:
|
|
18
18
|
def __repr__(self):
|
|
19
|
-
return "<Flow {
|
|
19
|
+
return "<Flow {} '{}' ({}) Project={} createdAt={}".format(
|
|
20
20
|
self._id, self.name, self.description, self.project_id, self.created_at
|
|
21
21
|
)
|
|
22
22
|
|
|
@@ -24,13 +24,13 @@ class FlowItem(object):
|
|
|
24
24
|
self._webpage_url: Optional[str] = None
|
|
25
25
|
self._created_at: Optional[datetime.datetime] = None
|
|
26
26
|
self._id: Optional[str] = None
|
|
27
|
-
self._initial_tags:
|
|
27
|
+
self._initial_tags: set[str] = set()
|
|
28
28
|
self._project_name: Optional[str] = None
|
|
29
29
|
self._updated_at: Optional[datetime.datetime] = None
|
|
30
30
|
self.name: Optional[str] = name
|
|
31
31
|
self.owner_id: Optional[str] = None
|
|
32
32
|
self.project_id: str = project_id
|
|
33
|
-
self.tags:
|
|
33
|
+
self.tags: set[str] = set()
|
|
34
34
|
self.description: Optional[str] = None
|
|
35
35
|
|
|
36
36
|
self._connections: Optional[ConnectionItem] = None
|
|
@@ -170,7 +170,7 @@ class FlowItem(object):
|
|
|
170
170
|
self.owner_id = owner_id
|
|
171
171
|
|
|
172
172
|
@classmethod
|
|
173
|
-
def from_response(cls, resp, ns) ->
|
|
173
|
+
def from_response(cls, resp, ns) -> list["FlowItem"]:
|
|
174
174
|
all_flow_items = list()
|
|
175
175
|
parsed_response = fromstring(resp)
|
|
176
176
|
all_flow_xml = parsed_response.findall(".//t:flow", namespaces=ns)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import itertools
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Optional
|
|
4
4
|
|
|
5
5
|
from defusedxml.ElementTree import fromstring
|
|
6
6
|
|
|
7
7
|
from tableauserverclient.datetime_helpers import parse_datetime
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class FlowRunItem
|
|
10
|
+
class FlowRunItem:
|
|
11
11
|
def __init__(self) -> None:
|
|
12
12
|
self._id: str = ""
|
|
13
13
|
self._flow_id: Optional[str] = None
|
|
@@ -71,7 +71,7 @@ class FlowRunItem(object):
|
|
|
71
71
|
self._background_job_id = background_job_id
|
|
72
72
|
|
|
73
73
|
@classmethod
|
|
74
|
-
def from_response(cls:
|
|
74
|
+
def from_response(cls: type["FlowRunItem"], resp: bytes, ns: Optional[dict]) -> list["FlowRunItem"]:
|
|
75
75
|
all_flowrun_items = list()
|
|
76
76
|
parsed_response = fromstring(resp)
|
|
77
77
|
all_flowrun_xml = itertools.chain(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Callable,
|
|
1
|
+
from typing import Callable, Optional, TYPE_CHECKING
|
|
2
2
|
|
|
3
3
|
from defusedxml.ElementTree import fromstring
|
|
4
4
|
|
|
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
|
|
11
11
|
from tableauserverclient.server import Pager
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class GroupItem
|
|
14
|
+
class GroupItem:
|
|
15
15
|
tag_name: str = "group"
|
|
16
16
|
|
|
17
17
|
class LicenseMode:
|
|
@@ -27,7 +27,7 @@ class GroupItem(object):
|
|
|
27
27
|
self.domain_name: Optional[str] = domain_name
|
|
28
28
|
|
|
29
29
|
def __repr__(self):
|
|
30
|
-
return "{
|
|
30
|
+
return f"{self.__class__.__name__}({self.__dict__!r})"
|
|
31
31
|
|
|
32
32
|
@property
|
|
33
33
|
def domain_name(self) -> Optional[str]:
|
|
@@ -79,7 +79,7 @@ class GroupItem(object):
|
|
|
79
79
|
self._users = users
|
|
80
80
|
|
|
81
81
|
@classmethod
|
|
82
|
-
def from_response(cls, resp, ns) ->
|
|
82
|
+
def from_response(cls, resp, ns) -> list["GroupItem"]:
|
|
83
83
|
all_group_items = list()
|
|
84
84
|
parsed_response = fromstring(resp)
|
|
85
85
|
all_group_xml = parsed_response.findall(".//t:group", namespaces=ns)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
import xml.etree.ElementTree as ET
|
|
3
|
+
|
|
4
|
+
from defusedxml.ElementTree import fromstring
|
|
5
|
+
|
|
6
|
+
from tableauserverclient.models.group_item import GroupItem
|
|
7
|
+
from tableauserverclient.models.reference_item import ResourceReference
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GroupSetItem:
|
|
11
|
+
tag_name: str = "groupSet"
|
|
12
|
+
|
|
13
|
+
def __init__(self, name: Optional[str] = None) -> None:
|
|
14
|
+
self.name = name
|
|
15
|
+
self.id: Optional[str] = None
|
|
16
|
+
self.groups: list["GroupItem"] = []
|
|
17
|
+
self.group_count: int = 0
|
|
18
|
+
|
|
19
|
+
def __str__(self) -> str:
|
|
20
|
+
name = self.name
|
|
21
|
+
id = self.id
|
|
22
|
+
return f"<{self.__class__.__qualname__}({name=}, {id=})>"
|
|
23
|
+
|
|
24
|
+
def __repr__(self) -> str:
|
|
25
|
+
return self.__str__()
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_response(cls, response: bytes, ns: dict[str, str]) -> list["GroupSetItem"]:
|
|
29
|
+
parsed_response = fromstring(response)
|
|
30
|
+
all_groupset_xml = parsed_response.findall(".//t:groupSet", namespaces=ns)
|
|
31
|
+
return [cls.from_xml(xml, ns) for xml in all_groupset_xml]
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_xml(cls, groupset_xml: ET.Element, ns: dict[str, str]) -> "GroupSetItem":
|
|
35
|
+
def get_group(group_xml: ET.Element) -> GroupItem:
|
|
36
|
+
group_item = GroupItem()
|
|
37
|
+
group_item._id = group_xml.get("id")
|
|
38
|
+
group_item.name = group_xml.get("name")
|
|
39
|
+
return group_item
|
|
40
|
+
|
|
41
|
+
group_set_item = cls()
|
|
42
|
+
group_set_item.name = groupset_xml.get("name")
|
|
43
|
+
group_set_item.id = groupset_xml.get("id")
|
|
44
|
+
group_set_item.group_count = int(count) if (count := groupset_xml.get("groupCount")) else 0
|
|
45
|
+
group_set_item.groups = [
|
|
46
|
+
get_group(group_xml) for group_xml in groupset_xml.findall(".//t:group", namespaces=ns)
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
return group_set_item
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def as_reference(id_: str) -> ResourceReference:
|
|
53
|
+
return ResourceReference(id_, GroupSetItem.tag_name)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from .property_decorators import property_is_valid_time, property_not_nullable
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
class IntervalItem
|
|
4
|
+
class IntervalItem:
|
|
5
5
|
class Frequency:
|
|
6
6
|
Hourly = "Hourly"
|
|
7
7
|
Daily = "Daily"
|
|
@@ -25,7 +25,7 @@ class IntervalItem(object):
|
|
|
25
25
|
LastDay = "LastDay"
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
class HourlyInterval
|
|
28
|
+
class HourlyInterval:
|
|
29
29
|
def __init__(self, start_time, end_time, interval_value):
|
|
30
30
|
self.start_time = start_time
|
|
31
31
|
self.end_time = end_time
|
|
@@ -73,12 +73,12 @@ class HourlyInterval(object):
|
|
|
73
73
|
for interval in intervals:
|
|
74
74
|
# if an hourly interval is a string, then it is a weekDay interval
|
|
75
75
|
if isinstance(interval, str) and not interval.isnumeric() and not hasattr(IntervalItem.Day, interval):
|
|
76
|
-
error = "Invalid weekDay interval {}"
|
|
76
|
+
error = f"Invalid weekDay interval {interval}"
|
|
77
77
|
raise ValueError(error)
|
|
78
78
|
|
|
79
79
|
# if an hourly interval is a number, it is an hours or minutes interval
|
|
80
80
|
if isinstance(interval, (int, float)) and float(interval) not in VALID_INTERVALS:
|
|
81
|
-
error = "Invalid interval {} not in {
|
|
81
|
+
error = f"Invalid interval {interval} not in {str(VALID_INTERVALS)}"
|
|
82
82
|
raise ValueError(error)
|
|
83
83
|
|
|
84
84
|
self._interval = intervals
|
|
@@ -108,7 +108,7 @@ class HourlyInterval(object):
|
|
|
108
108
|
return interval_type_pairs
|
|
109
109
|
|
|
110
110
|
|
|
111
|
-
class DailyInterval
|
|
111
|
+
class DailyInterval:
|
|
112
112
|
def __init__(self, start_time, *interval_values):
|
|
113
113
|
self.start_time = start_time
|
|
114
114
|
self.interval = interval_values
|
|
@@ -141,12 +141,12 @@ class DailyInterval(object):
|
|
|
141
141
|
for interval in intervals:
|
|
142
142
|
# if an hourly interval is a string, then it is a weekDay interval
|
|
143
143
|
if isinstance(interval, str) and not interval.isnumeric() and not hasattr(IntervalItem.Day, interval):
|
|
144
|
-
error = "Invalid weekDay interval {}"
|
|
144
|
+
error = f"Invalid weekDay interval {interval}"
|
|
145
145
|
raise ValueError(error)
|
|
146
146
|
|
|
147
147
|
# if an hourly interval is a number, it is an hours or minutes interval
|
|
148
148
|
if isinstance(interval, (int, float)) and float(interval) not in VALID_INTERVALS:
|
|
149
|
-
error = "Invalid interval {} not in {
|
|
149
|
+
error = f"Invalid interval {interval} not in {str(VALID_INTERVALS)}"
|
|
150
150
|
raise ValueError(error)
|
|
151
151
|
|
|
152
152
|
self._interval = intervals
|
|
@@ -176,7 +176,7 @@ class DailyInterval(object):
|
|
|
176
176
|
return interval_type_pairs
|
|
177
177
|
|
|
178
178
|
|
|
179
|
-
class WeeklyInterval
|
|
179
|
+
class WeeklyInterval:
|
|
180
180
|
def __init__(self, start_time, *interval_values):
|
|
181
181
|
self.start_time = start_time
|
|
182
182
|
self.interval = interval_values
|
|
@@ -213,7 +213,7 @@ class WeeklyInterval(object):
|
|
|
213
213
|
return [(IntervalItem.Occurrence.WeekDay, day) for day in self.interval]
|
|
214
214
|
|
|
215
215
|
|
|
216
|
-
class MonthlyInterval
|
|
216
|
+
class MonthlyInterval:
|
|
217
217
|
def __init__(self, start_time, interval_value):
|
|
218
218
|
self.start_time = start_time
|
|
219
219
|
|
|
@@ -246,21 +246,34 @@ class MonthlyInterval(object):
|
|
|
246
246
|
|
|
247
247
|
@interval.setter
|
|
248
248
|
def interval(self, interval_values):
|
|
249
|
-
#
|
|
250
|
-
#
|
|
251
|
-
#
|
|
252
|
-
#
|
|
253
|
-
|
|
249
|
+
# Valid monthly intervals strings can contain any of the following
|
|
250
|
+
# day numbers (1-31) (integer or string)
|
|
251
|
+
# relative day within the month (First, Second, ... Last)
|
|
252
|
+
# week days (Sunday, Monday, ... LastDay)
|
|
253
|
+
VALID_INTERVALS = [
|
|
254
|
+
"Sunday",
|
|
255
|
+
"Monday",
|
|
256
|
+
"Tuesday",
|
|
257
|
+
"Wednesday",
|
|
258
|
+
"Thursday",
|
|
259
|
+
"Friday",
|
|
260
|
+
"Saturday",
|
|
261
|
+
"LastDay",
|
|
262
|
+
"First",
|
|
263
|
+
"Second",
|
|
264
|
+
"Third",
|
|
265
|
+
"Fourth",
|
|
266
|
+
"Fifth",
|
|
267
|
+
"Last",
|
|
268
|
+
]
|
|
269
|
+
for value in range(1, 32):
|
|
270
|
+
VALID_INTERVALS.append(str(value))
|
|
271
|
+
VALID_INTERVALS.append(value)
|
|
272
|
+
|
|
254
273
|
for interval_value in interval_values:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
try:
|
|
259
|
-
if not (1 <= int(interval_value) <= 31):
|
|
260
|
-
raise ValueError(error)
|
|
261
|
-
except ValueError:
|
|
262
|
-
if interval_value != "LastDay":
|
|
263
|
-
raise ValueError(error)
|
|
274
|
+
if interval_value not in VALID_INTERVALS:
|
|
275
|
+
error = f"Invalid monthly interval: {interval_value}"
|
|
276
|
+
raise ValueError(error)
|
|
264
277
|
|
|
265
278
|
self._interval = interval_values
|
|
266
279
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import datetime
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Optional
|
|
3
3
|
|
|
4
4
|
from defusedxml.ElementTree import fromstring
|
|
5
5
|
|
|
6
6
|
from tableauserverclient.datetime_helpers import parse_datetime
|
|
7
|
-
from .flow_run_item import FlowRunItem
|
|
7
|
+
from tableauserverclient.models.flow_run_item import FlowRunItem
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class JobItem
|
|
10
|
+
class JobItem:
|
|
11
11
|
class FinishCode:
|
|
12
12
|
"""
|
|
13
13
|
Status codes as documented on
|
|
@@ -27,12 +27,14 @@ class JobItem(object):
|
|
|
27
27
|
started_at: Optional[datetime.datetime] = None,
|
|
28
28
|
completed_at: Optional[datetime.datetime] = None,
|
|
29
29
|
finish_code: int = 0,
|
|
30
|
-
notes: Optional[
|
|
30
|
+
notes: Optional[list[str]] = None,
|
|
31
31
|
mode: Optional[str] = None,
|
|
32
32
|
workbook_id: Optional[str] = None,
|
|
33
33
|
datasource_id: Optional[str] = None,
|
|
34
34
|
flow_run: Optional[FlowRunItem] = None,
|
|
35
35
|
updated_at: Optional[datetime.datetime] = None,
|
|
36
|
+
workbook_name: Optional[str] = None,
|
|
37
|
+
datasource_name: Optional[str] = None,
|
|
36
38
|
):
|
|
37
39
|
self._id = id_
|
|
38
40
|
self._type = job_type
|
|
@@ -41,12 +43,14 @@ class JobItem(object):
|
|
|
41
43
|
self._started_at = started_at
|
|
42
44
|
self._completed_at = completed_at
|
|
43
45
|
self._finish_code = finish_code
|
|
44
|
-
self._notes:
|
|
46
|
+
self._notes: list[str] = notes or []
|
|
45
47
|
self._mode = mode
|
|
46
48
|
self._workbook_id = workbook_id
|
|
47
49
|
self._datasource_id = datasource_id
|
|
48
50
|
self._flow_run = flow_run
|
|
49
51
|
self._updated_at = updated_at
|
|
52
|
+
self._workbook_name = workbook_name
|
|
53
|
+
self._datasource_name = datasource_name
|
|
50
54
|
|
|
51
55
|
@property
|
|
52
56
|
def id(self) -> str:
|
|
@@ -77,7 +81,7 @@ class JobItem(object):
|
|
|
77
81
|
return self._finish_code
|
|
78
82
|
|
|
79
83
|
@property
|
|
80
|
-
def notes(self) ->
|
|
84
|
+
def notes(self) -> list[str]:
|
|
81
85
|
return self._notes
|
|
82
86
|
|
|
83
87
|
@property
|
|
@@ -117,6 +121,14 @@ class JobItem(object):
|
|
|
117
121
|
def updated_at(self) -> Optional[datetime.datetime]:
|
|
118
122
|
return self._updated_at
|
|
119
123
|
|
|
124
|
+
@property
|
|
125
|
+
def workbook_name(self) -> Optional[str]:
|
|
126
|
+
return self._workbook_name
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def datasource_name(self) -> Optional[str]:
|
|
130
|
+
return self._datasource_name
|
|
131
|
+
|
|
120
132
|
def __str__(self):
|
|
121
133
|
return (
|
|
122
134
|
"<Job#{_id} {_type} created_at({_created_at}) started_at({_started_at}) updated_at({_updated_at}) completed_at({_completed_at})"
|
|
@@ -127,7 +139,7 @@ class JobItem(object):
|
|
|
127
139
|
return self.__str__() + " { " + ", ".join(" % s: % s" % item for item in vars(self).items()) + "}"
|
|
128
140
|
|
|
129
141
|
@classmethod
|
|
130
|
-
def from_response(cls, xml, ns) ->
|
|
142
|
+
def from_response(cls, xml, ns) -> list["JobItem"]:
|
|
131
143
|
parsed_response = fromstring(xml)
|
|
132
144
|
all_tasks_xml = parsed_response.findall(".//t:job", namespaces=ns)
|
|
133
145
|
|
|
@@ -148,8 +160,10 @@ class JobItem(object):
|
|
|
148
160
|
mode = element.get("mode", None)
|
|
149
161
|
workbook = element.find(".//t:workbook[@id]", namespaces=ns)
|
|
150
162
|
workbook_id = workbook.get("id") if workbook is not None else None
|
|
163
|
+
workbook_name = workbook.get("name") if workbook is not None else None
|
|
151
164
|
datasource = element.find(".//t:datasource[@id]", namespaces=ns)
|
|
152
165
|
datasource_id = datasource.get("id") if datasource is not None else None
|
|
166
|
+
datasource_name = datasource.get("name") if datasource is not None else None
|
|
153
167
|
flow_run = None
|
|
154
168
|
updated_at = parse_datetime(element.get("updatedAt", None))
|
|
155
169
|
for flow_job in element.findall(".//t:runFlowJobType", namespaces=ns):
|
|
@@ -172,10 +186,12 @@ class JobItem(object):
|
|
|
172
186
|
datasource_id,
|
|
173
187
|
flow_run,
|
|
174
188
|
updated_at,
|
|
189
|
+
workbook_name,
|
|
190
|
+
datasource_name,
|
|
175
191
|
)
|
|
176
192
|
|
|
177
193
|
|
|
178
|
-
class BackgroundJobItem
|
|
194
|
+
class BackgroundJobItem:
|
|
179
195
|
class Status:
|
|
180
196
|
Pending: str = "Pending"
|
|
181
197
|
InProgress: str = "InProgress"
|
|
@@ -206,7 +222,7 @@ class BackgroundJobItem(object):
|
|
|
206
222
|
self._subtitle = subtitle
|
|
207
223
|
|
|
208
224
|
def __str__(self):
|
|
209
|
-
return f"<{self.__class__.
|
|
225
|
+
return f"<{self.__class__.__qualname__} {self._id} {self._type}>"
|
|
210
226
|
|
|
211
227
|
def __repr__(self):
|
|
212
228
|
return self.__str__() + " { " + ", ".join(" % s: % s" % item for item in vars(self).items()) + "}"
|
|
@@ -254,7 +270,7 @@ class BackgroundJobItem(object):
|
|
|
254
270
|
return self._priority
|
|
255
271
|
|
|
256
272
|
@classmethod
|
|
257
|
-
def from_response(cls, xml, ns) ->
|
|
273
|
+
def from_response(cls, xml, ns) -> list["BackgroundJobItem"]:
|
|
258
274
|
parsed_response = fromstring(xml)
|
|
259
275
|
all_tasks_xml = parsed_response.findall(".//t:backgroundJob", namespaces=ns)
|
|
260
276
|
return [cls._parse_element(x, ns) for x in all_tasks_xml]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from defusedxml.ElementTree import fromstring
|
|
5
|
+
|
|
6
|
+
from tableauserverclient.datetime_helpers import parse_datetime
|
|
7
|
+
from tableauserverclient.models.schedule_item import ScheduleItem
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LinkedTaskItem:
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self.id: Optional[str] = None
|
|
13
|
+
self.num_steps: Optional[int] = None
|
|
14
|
+
self.schedule: Optional[ScheduleItem] = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_response(cls, resp: bytes, namespace) -> list["LinkedTaskItem"]:
|
|
18
|
+
parsed_response = fromstring(resp)
|
|
19
|
+
return [
|
|
20
|
+
cls._parse_element(x, namespace)
|
|
21
|
+
for x in parsed_response.findall(".//t:linkedTasks[@id]", namespaces=namespace)
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def _parse_element(cls, xml, namespace) -> "LinkedTaskItem":
|
|
26
|
+
task = cls()
|
|
27
|
+
task.id = xml.get("id")
|
|
28
|
+
task.num_steps = int(xml.get("numSteps"))
|
|
29
|
+
task.schedule = ScheduleItem.from_element(xml, namespace)[0]
|
|
30
|
+
return task
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class LinkedTaskStepItem:
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
self.id: Optional[str] = None
|
|
36
|
+
self.step_number: Optional[int] = None
|
|
37
|
+
self.stop_downstream_on_failure: Optional[bool] = None
|
|
38
|
+
self.task_details: list[LinkedTaskFlowRunItem] = []
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_task_xml(cls, xml, namespace) -> list["LinkedTaskStepItem"]:
|
|
42
|
+
return [cls._parse_element(x, namespace) for x in xml.findall(".//t:linkedTaskSteps[@id]", namespace)]
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def _parse_element(cls, xml, namespace) -> "LinkedTaskStepItem":
|
|
46
|
+
step = cls()
|
|
47
|
+
step.id = xml.get("id")
|
|
48
|
+
step.step_number = int(xml.get("stepNumber"))
|
|
49
|
+
step.stop_downstream_on_failure = string_to_bool(xml.get("stopDownstreamTasksOnFailure"))
|
|
50
|
+
step.task_details = LinkedTaskFlowRunItem._parse_element(xml, namespace)
|
|
51
|
+
return step
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class LinkedTaskFlowRunItem:
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
self.flow_run_id: Optional[str] = None
|
|
57
|
+
self.flow_run_priority: Optional[int] = None
|
|
58
|
+
self.flow_run_consecutive_failed_count: Optional[int] = None
|
|
59
|
+
self.flow_run_task_type: Optional[str] = None
|
|
60
|
+
self.flow_id: Optional[str] = None
|
|
61
|
+
self.flow_name: Optional[str] = None
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def _parse_element(cls, xml, namespace) -> list["LinkedTaskFlowRunItem"]:
|
|
65
|
+
all_tasks = []
|
|
66
|
+
for flow_run in xml.findall(".//t:flowRun[@id]", namespace):
|
|
67
|
+
task = cls()
|
|
68
|
+
task.flow_run_id = flow_run.get("id")
|
|
69
|
+
task.flow_run_priority = int(flow_run.get("priority"))
|
|
70
|
+
task.flow_run_consecutive_failed_count = int(flow_run.get("consecutiveFailedCount"))
|
|
71
|
+
task.flow_run_task_type = flow_run.get("type")
|
|
72
|
+
flow = flow_run.find(".//t:flow[@id]", namespace)
|
|
73
|
+
task.flow_id = flow.get("id")
|
|
74
|
+
task.flow_name = flow.get("name")
|
|
75
|
+
all_tasks.append(task)
|
|
76
|
+
|
|
77
|
+
return all_tasks
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class LinkedTaskJobItem:
|
|
81
|
+
def __init__(self) -> None:
|
|
82
|
+
self.id: Optional[str] = None
|
|
83
|
+
self.linked_task_id: Optional[str] = None
|
|
84
|
+
self.status: Optional[str] = None
|
|
85
|
+
self.created_at: Optional[dt.datetime] = None
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_response(cls, resp: bytes, namespace) -> "LinkedTaskJobItem":
|
|
89
|
+
parsed_response = fromstring(resp)
|
|
90
|
+
job = cls()
|
|
91
|
+
job_xml = parsed_response.find(".//t:linkedTaskJob[@id]", namespaces=namespace)
|
|
92
|
+
if job_xml is None:
|
|
93
|
+
raise ValueError("No linked task job found in response")
|
|
94
|
+
job.id = job_xml.get("id")
|
|
95
|
+
job.linked_task_id = job_xml.get("linkedTaskId")
|
|
96
|
+
job.status = job_xml.get("status")
|
|
97
|
+
job.created_at = parse_datetime(job_xml.get("createdAt"))
|
|
98
|
+
return job
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def string_to_bool(s: str) -> bool:
|
|
102
|
+
return s.lower() == "true"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import xml.etree.ElementTree as ET
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Optional
|
|
4
4
|
|
|
5
5
|
from tableauserverclient.datetime_helpers import parse_datetime
|
|
6
6
|
from .property_decorators import property_is_boolean, property_is_datetime
|
|
@@ -8,7 +8,7 @@ from .tag_item import TagItem
|
|
|
8
8
|
from .permissions_item import Permission
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class MetricItem
|
|
11
|
+
class MetricItem:
|
|
12
12
|
def __init__(self, name: Optional[str] = None):
|
|
13
13
|
self._id: Optional[str] = None
|
|
14
14
|
self._name: Optional[str] = name
|
|
@@ -21,8 +21,8 @@ class MetricItem(object):
|
|
|
21
21
|
self._project_name: Optional[str] = None
|
|
22
22
|
self._owner_id: Optional[str] = None
|
|
23
23
|
self._view_id: Optional[str] = None
|
|
24
|
-
self._initial_tags:
|
|
25
|
-
self.tags:
|
|
24
|
+
self._initial_tags: set[str] = set()
|
|
25
|
+
self.tags: set[str] = set()
|
|
26
26
|
self._permissions: Optional[Permission] = None
|
|
27
27
|
|
|
28
28
|
@property
|
|
@@ -126,7 +126,7 @@ class MetricItem(object):
|
|
|
126
126
|
cls,
|
|
127
127
|
resp: bytes,
|
|
128
128
|
ns,
|
|
129
|
-
) ->
|
|
129
|
+
) -> list["MetricItem"]:
|
|
130
130
|
all_metric_items = list()
|
|
131
131
|
parsed_response = ET.fromstring(resp)
|
|
132
132
|
all_metric_xml = parsed_response.findall(".//t:metric", namespaces=ns)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import xml.etree.ElementTree as ET
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Optional
|
|
3
3
|
|
|
4
4
|
from defusedxml.ElementTree import fromstring
|
|
5
5
|
|
|
6
|
-
from .exceptions import UnknownGranteeTypeError, UnpopulatedPropertyError
|
|
7
|
-
from .group_item import GroupItem
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
6
|
+
from tableauserverclient.models.exceptions import UnknownGranteeTypeError, UnpopulatedPropertyError
|
|
7
|
+
from tableauserverclient.models.group_item import GroupItem
|
|
8
|
+
from tableauserverclient.models.groupset_item import GroupSetItem
|
|
9
|
+
from tableauserverclient.models.reference_item import ResourceReference
|
|
10
|
+
from tableauserverclient.models.user_item import UserItem
|
|
10
11
|
|
|
11
12
|
from tableauserverclient.helpers.logging import logger
|
|
12
13
|
|
|
@@ -35,23 +36,25 @@ class Permission:
|
|
|
35
36
|
ShareView = "ShareView"
|
|
36
37
|
ViewComments = "ViewComments"
|
|
37
38
|
ViewUnderlyingData = "ViewUnderlyingData"
|
|
39
|
+
VizqlDataApiAccess = "VizqlDataApiAccess"
|
|
38
40
|
WebAuthoring = "WebAuthoring"
|
|
39
41
|
Write = "Write"
|
|
40
42
|
RunExplainData = "RunExplainData"
|
|
41
43
|
CreateRefreshMetrics = "CreateRefreshMetrics"
|
|
42
44
|
SaveAs = "SaveAs"
|
|
45
|
+
PulseMetricDefine = "PulseMetricDefine"
|
|
43
46
|
|
|
44
47
|
def __repr__(self):
|
|
45
48
|
return "<Enum Capability: AddComment | ChangeHierarchy | ChangePermission ... (17 more) >"
|
|
46
49
|
|
|
47
50
|
|
|
48
51
|
class PermissionsRule:
|
|
49
|
-
def __init__(self, grantee: ResourceReference, capabilities:
|
|
52
|
+
def __init__(self, grantee: ResourceReference, capabilities: dict[str, str]) -> None:
|
|
50
53
|
self.grantee = grantee
|
|
51
54
|
self.capabilities = capabilities
|
|
52
55
|
|
|
53
56
|
def __repr__(self):
|
|
54
|
-
return "<PermissionsRule grantee={}, capabilities={}>"
|
|
57
|
+
return f"<PermissionsRule grantee={self.grantee}, capabilities={self.capabilities}>"
|
|
55
58
|
|
|
56
59
|
def __eq__(self, other: object) -> bool:
|
|
57
60
|
if not hasattr(other, "grantee") or not hasattr(other, "capabilities"):
|
|
@@ -65,7 +68,7 @@ class PermissionsRule:
|
|
|
65
68
|
if self.capabilities == other.capabilities:
|
|
66
69
|
return self
|
|
67
70
|
|
|
68
|
-
capabilities =
|
|
71
|
+
capabilities = {*self.capabilities.keys(), *other.capabilities.keys()}
|
|
69
72
|
new_capabilities = {}
|
|
70
73
|
for capability in capabilities:
|
|
71
74
|
if (self.capabilities.get(capability), other.capabilities.get(capability)) == (
|
|
@@ -85,7 +88,7 @@ class PermissionsRule:
|
|
|
85
88
|
if self.capabilities == other.capabilities:
|
|
86
89
|
return self
|
|
87
90
|
|
|
88
|
-
capabilities =
|
|
91
|
+
capabilities = {*self.capabilities.keys(), *other.capabilities.keys()}
|
|
89
92
|
new_capabilities = {}
|
|
90
93
|
for capability in capabilities:
|
|
91
94
|
if Permission.Mode.Allow in (self.capabilities.get(capability), other.capabilities.get(capability)):
|
|
@@ -99,14 +102,14 @@ class PermissionsRule:
|
|
|
99
102
|
return PermissionsRule(self.grantee, new_capabilities)
|
|
100
103
|
|
|
101
104
|
@classmethod
|
|
102
|
-
def from_response(cls, resp, ns=None) ->
|
|
105
|
+
def from_response(cls, resp, ns=None) -> list["PermissionsRule"]:
|
|
103
106
|
parsed_response = fromstring(resp)
|
|
104
107
|
|
|
105
108
|
rules = []
|
|
106
109
|
permissions_rules_list_xml = parsed_response.findall(".//t:granteeCapabilities", namespaces=ns)
|
|
107
110
|
|
|
108
111
|
for grantee_capability_xml in permissions_rules_list_xml:
|
|
109
|
-
capability_dict:
|
|
112
|
+
capability_dict: dict[str, str] = {}
|
|
110
113
|
|
|
111
114
|
grantee = PermissionsRule._parse_grantee_element(grantee_capability_xml, ns)
|
|
112
115
|
|
|
@@ -115,7 +118,7 @@ class PermissionsRule:
|
|
|
115
118
|
mode = capability_xml.get("mode")
|
|
116
119
|
|
|
117
120
|
if name is None or mode is None:
|
|
118
|
-
logger.error("Capability was not valid: {}"
|
|
121
|
+
logger.error(f"Capability was not valid: {capability_xml}")
|
|
119
122
|
raise UnpopulatedPropertyError()
|
|
120
123
|
else:
|
|
121
124
|
capability_dict[name] = mode
|
|
@@ -126,7 +129,7 @@ class PermissionsRule:
|
|
|
126
129
|
return rules
|
|
127
130
|
|
|
128
131
|
@staticmethod
|
|
129
|
-
def _parse_grantee_element(grantee_capability_xml: ET.Element, ns: Optional[
|
|
132
|
+
def _parse_grantee_element(grantee_capability_xml: ET.Element, ns: Optional[dict[str, str]]) -> ResourceReference:
|
|
130
133
|
"""Use Xpath magic and some string splitting to get the right object type from the xml"""
|
|
131
134
|
|
|
132
135
|
# Get the first element in the tree with an 'id' attribute
|
|
@@ -142,7 +145,9 @@ class PermissionsRule:
|
|
|
142
145
|
grantee = UserItem.as_reference(grantee_id)
|
|
143
146
|
elif grantee_type == "group":
|
|
144
147
|
grantee = GroupItem.as_reference(grantee_id)
|
|
148
|
+
elif grantee_type == "groupSet":
|
|
149
|
+
grantee = GroupSetItem.as_reference(grantee_id)
|
|
145
150
|
else:
|
|
146
|
-
raise UnknownGranteeTypeError("No support for grantee type of {}"
|
|
151
|
+
raise UnknownGranteeTypeError(f"No support for grantee type of {grantee_type}")
|
|
147
152
|
|
|
148
153
|
return grantee
|