tableauserverclient 0.29__py3-none-any.whl → 0.30.post0.dev1__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 +1 -0
- tableauserverclient/_version.py +3 -3
- tableauserverclient/models/__init__.py +1 -0
- tableauserverclient/models/data_freshness_policy_item.py +210 -0
- tableauserverclient/models/interval_item.py +2 -2
- tableauserverclient/models/property_decorators.py +6 -11
- tableauserverclient/models/schedule_item.py +1 -0
- tableauserverclient/models/task_item.py +1 -0
- tableauserverclient/models/view_item.py +34 -0
- tableauserverclient/models/workbook_item.py +34 -4
- tableauserverclient/server/endpoint/__init__.py +1 -0
- tableauserverclient/server/endpoint/datasources_endpoint.py +5 -3
- tableauserverclient/server/endpoint/endpoint.py +3 -40
- tableauserverclient/server/endpoint/flow_task_endpoint.py +29 -0
- tableauserverclient/server/endpoint/flows_endpoint.py +5 -3
- tableauserverclient/server/endpoint/workbooks_endpoint.py +14 -4
- tableauserverclient/server/pager.py +5 -1
- tableauserverclient/server/request_factory.py +92 -4
- tableauserverclient/server/request_options.py +47 -3
- tableauserverclient/server/server.py +2 -0
- {tableauserverclient-0.29.dist-info → tableauserverclient-0.30.post0.dev1.dist-info}/METADATA +2 -1
- {tableauserverclient-0.29.dist-info → tableauserverclient-0.30.post0.dev1.dist-info}/RECORD +26 -24
- {tableauserverclient-0.29.dist-info → tableauserverclient-0.30.post0.dev1.dist-info}/WHEEL +1 -1
- {tableauserverclient-0.29.dist-info → tableauserverclient-0.30.post0.dev1.dist-info}/LICENSE +0 -0
- {tableauserverclient-0.29.dist-info → tableauserverclient-0.30.post0.dev1.dist-info}/LICENSE.versioneer +0 -0
- {tableauserverclient-0.29.dist-info → tableauserverclient-0.30.post0.dev1.dist-info}/top_level.txt +0 -0
tableauserverclient/__init__.py
CHANGED
tableauserverclient/_version.py
CHANGED
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2024-
|
|
11
|
+
"date": "2024-06-03T12:52:39-0700",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "0.
|
|
14
|
+
"full-revisionid": "4018a0ffc01bfa7c5b0024c6f112ced158d420b9",
|
|
15
|
+
"version": "0.30.post0.dev1"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -5,6 +5,7 @@ from .custom_view_item import CustomViewItem
|
|
|
5
5
|
from .data_acceleration_report_item import DataAccelerationReportItem
|
|
6
6
|
from .data_alert_item import DataAlertItem
|
|
7
7
|
from .database_item import DatabaseItem
|
|
8
|
+
from .data_freshness_policy_item import DataFreshnessPolicyItem
|
|
8
9
|
from .datasource_item import DatasourceItem
|
|
9
10
|
from .dqw_item import DQWItem
|
|
10
11
|
from .exceptions import UnpopulatedPropertyError
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import xml.etree.ElementTree as ET
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Union, List
|
|
4
|
+
from tableauserverclient.models.property_decorators import property_is_enum, property_not_nullable
|
|
5
|
+
from .interval_item import IntervalItem
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DataFreshnessPolicyItem:
|
|
9
|
+
class Option:
|
|
10
|
+
AlwaysLive = "AlwaysLive"
|
|
11
|
+
SiteDefault = "SiteDefault"
|
|
12
|
+
FreshEvery = "FreshEvery"
|
|
13
|
+
FreshAt = "FreshAt"
|
|
14
|
+
|
|
15
|
+
class FreshEvery:
|
|
16
|
+
class Frequency:
|
|
17
|
+
Minutes = "Minutes"
|
|
18
|
+
Hours = "Hours"
|
|
19
|
+
Days = "Days"
|
|
20
|
+
Weeks = "Weeks"
|
|
21
|
+
|
|
22
|
+
def __init__(self, frequency: str, value: int):
|
|
23
|
+
self.frequency: str = frequency
|
|
24
|
+
self.value: int = value
|
|
25
|
+
|
|
26
|
+
def __repr__(self):
|
|
27
|
+
return "<FreshEvery frequency={_frequency} value={_value}>".format(**vars(self))
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def frequency(self) -> str:
|
|
31
|
+
return self._frequency
|
|
32
|
+
|
|
33
|
+
@frequency.setter
|
|
34
|
+
@property_is_enum(Frequency)
|
|
35
|
+
def frequency(self, value: str):
|
|
36
|
+
self._frequency = value
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_xml_element(cls, fresh_every_schedule_elem: ET.Element):
|
|
40
|
+
frequency = fresh_every_schedule_elem.get("frequency", None)
|
|
41
|
+
value_str = fresh_every_schedule_elem.get("value", None)
|
|
42
|
+
if (frequency is None) or (value_str is None):
|
|
43
|
+
return None
|
|
44
|
+
value = int(value_str)
|
|
45
|
+
return DataFreshnessPolicyItem.FreshEvery(frequency, value)
|
|
46
|
+
|
|
47
|
+
class FreshAt:
|
|
48
|
+
class Frequency:
|
|
49
|
+
Day = "Day"
|
|
50
|
+
Week = "Week"
|
|
51
|
+
Month = "Month"
|
|
52
|
+
|
|
53
|
+
def __init__(self, frequency: str, time: str, timezone, interval_item: Optional[List[str]] = None):
|
|
54
|
+
self.frequency = frequency
|
|
55
|
+
self.time = time
|
|
56
|
+
self.timezone = timezone
|
|
57
|
+
self.interval_item: Optional[List[str]] = interval_item
|
|
58
|
+
|
|
59
|
+
def __repr__(self):
|
|
60
|
+
return (
|
|
61
|
+
"<FreshAt frequency={_frequency} time={_time}> timezone={_timezone} " "interval_item={_interval_time}"
|
|
62
|
+
).format(**vars(self))
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def interval_item(self) -> Optional[List[str]]:
|
|
66
|
+
return self._interval_item
|
|
67
|
+
|
|
68
|
+
@interval_item.setter
|
|
69
|
+
def interval_item(self, value: List[str]):
|
|
70
|
+
self._interval_item = value
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def time(self):
|
|
74
|
+
return self._time
|
|
75
|
+
|
|
76
|
+
@time.setter
|
|
77
|
+
@property_not_nullable
|
|
78
|
+
def time(self, value):
|
|
79
|
+
self._time = value
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def timezone(self) -> str:
|
|
83
|
+
return self._timezone
|
|
84
|
+
|
|
85
|
+
@timezone.setter
|
|
86
|
+
def timezone(self, value: str):
|
|
87
|
+
self._timezone = value
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def frequency(self) -> str:
|
|
91
|
+
return self._frequency
|
|
92
|
+
|
|
93
|
+
@frequency.setter
|
|
94
|
+
@property_is_enum(Frequency)
|
|
95
|
+
def frequency(self, value: str):
|
|
96
|
+
self._frequency = value
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_xml_element(cls, fresh_at_schedule_elem: ET.Element, ns):
|
|
100
|
+
frequency = fresh_at_schedule_elem.get("frequency", None)
|
|
101
|
+
time = fresh_at_schedule_elem.get("time", None)
|
|
102
|
+
if (frequency is None) or (time is None):
|
|
103
|
+
return None
|
|
104
|
+
timezone = fresh_at_schedule_elem.get("timezone", None)
|
|
105
|
+
interval = parse_intervals(fresh_at_schedule_elem, frequency, ns)
|
|
106
|
+
return DataFreshnessPolicyItem.FreshAt(frequency, time, timezone, interval)
|
|
107
|
+
|
|
108
|
+
def __init__(self, option: str):
|
|
109
|
+
self.option = option
|
|
110
|
+
self.fresh_every_schedule: Optional[DataFreshnessPolicyItem.FreshEvery] = None
|
|
111
|
+
self.fresh_at_schedule: Optional[DataFreshnessPolicyItem.FreshAt] = None
|
|
112
|
+
|
|
113
|
+
def __repr__(self):
|
|
114
|
+
return "<DataFreshnessPolicy option={_option}>".format(**vars(self))
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def option(self) -> str:
|
|
118
|
+
return self._option
|
|
119
|
+
|
|
120
|
+
@option.setter
|
|
121
|
+
@property_is_enum(Option)
|
|
122
|
+
def option(self, value: str):
|
|
123
|
+
self._option = value
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def fresh_every_schedule(self) -> Optional[FreshEvery]:
|
|
127
|
+
return self._fresh_every_schedule
|
|
128
|
+
|
|
129
|
+
@fresh_every_schedule.setter
|
|
130
|
+
def fresh_every_schedule(self, value: FreshEvery):
|
|
131
|
+
self._fresh_every_schedule = value
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def fresh_at_schedule(self) -> Optional[FreshAt]:
|
|
135
|
+
return self._fresh_at_schedule
|
|
136
|
+
|
|
137
|
+
@fresh_at_schedule.setter
|
|
138
|
+
def fresh_at_schedule(self, value: FreshAt):
|
|
139
|
+
self._fresh_at_schedule = value
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def from_xml_element(cls, data_freshness_policy_elem, ns):
|
|
143
|
+
option = data_freshness_policy_elem.get("option", None)
|
|
144
|
+
if option is None:
|
|
145
|
+
return None
|
|
146
|
+
data_freshness_policy = DataFreshnessPolicyItem(option)
|
|
147
|
+
|
|
148
|
+
fresh_at_schedule = None
|
|
149
|
+
fresh_every_schedule = None
|
|
150
|
+
if option == "FreshAt":
|
|
151
|
+
fresh_at_schedule_elem = data_freshness_policy_elem.find(".//t:freshAtSchedule", namespaces=ns)
|
|
152
|
+
fresh_at_schedule = DataFreshnessPolicyItem.FreshAt.from_xml_element(fresh_at_schedule_elem, ns)
|
|
153
|
+
data_freshness_policy.fresh_at_schedule = fresh_at_schedule
|
|
154
|
+
elif option == "FreshEvery":
|
|
155
|
+
fresh_every_schedule_elem = data_freshness_policy_elem.find(".//t:freshEverySchedule", namespaces=ns)
|
|
156
|
+
fresh_every_schedule = DataFreshnessPolicyItem.FreshEvery.from_xml_element(fresh_every_schedule_elem)
|
|
157
|
+
data_freshness_policy.fresh_every_schedule = fresh_every_schedule
|
|
158
|
+
|
|
159
|
+
return data_freshness_policy
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def parse_intervals(intervals_elem, frequency, ns):
|
|
163
|
+
interval_elems = intervals_elem.findall(".//t:intervals/t:interval", namespaces=ns)
|
|
164
|
+
interval = []
|
|
165
|
+
for interval_elem in interval_elems:
|
|
166
|
+
interval.extend(interval_elem.attrib.items())
|
|
167
|
+
|
|
168
|
+
# No intervals expected for Day frequency
|
|
169
|
+
if frequency == DataFreshnessPolicyItem.FreshAt.Frequency.Day:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
if frequency == DataFreshnessPolicyItem.FreshAt.Frequency.Week:
|
|
173
|
+
interval_values = [(i[1]).title() for i in interval]
|
|
174
|
+
return parse_week_intervals(interval_values)
|
|
175
|
+
|
|
176
|
+
if frequency == DataFreshnessPolicyItem.FreshAt.Frequency.Month:
|
|
177
|
+
interval_values = [(i[1]) for i in interval]
|
|
178
|
+
return parse_month_intervals(interval_values)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def parse_week_intervals(interval_values):
|
|
182
|
+
# Using existing IntervalItem.Day to check valid weekday string
|
|
183
|
+
if not all(hasattr(IntervalItem.Day, day) for day in interval_values):
|
|
184
|
+
raise ValueError("Invalid week day defined " + str(interval_values))
|
|
185
|
+
return interval_values
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def parse_month_intervals(interval_values):
|
|
189
|
+
error = "Invalid interval value for a monthly frequency: {}.".format(interval_values)
|
|
190
|
+
|
|
191
|
+
# Month interval can have value either only ['LastDay'] or list of dates e.g. ["1", 20", "30"]
|
|
192
|
+
# First check if the list only have LastDay value. When using LastDay, there shouldn't be
|
|
193
|
+
# any other values, hence checking the first element of the list is enough.
|
|
194
|
+
# If the value is not "LastDay", we assume intervals is on list of dates format.
|
|
195
|
+
# We created this function instead of using existing MonthlyInterval because we allow list of dates interval,
|
|
196
|
+
|
|
197
|
+
intervals = []
|
|
198
|
+
if interval_values[0] == "LastDay":
|
|
199
|
+
intervals.append(interval_values[0])
|
|
200
|
+
else:
|
|
201
|
+
for interval in interval_values:
|
|
202
|
+
try:
|
|
203
|
+
if 1 <= int(interval) <= 31:
|
|
204
|
+
intervals.append(interval)
|
|
205
|
+
else:
|
|
206
|
+
raise ValueError(error)
|
|
207
|
+
except ValueError:
|
|
208
|
+
if interval_values[0] != "LastDay":
|
|
209
|
+
raise ValueError(error)
|
|
210
|
+
return intervals
|
|
@@ -69,7 +69,7 @@ class HourlyInterval(object):
|
|
|
69
69
|
|
|
70
70
|
@interval.setter
|
|
71
71
|
def interval(self, intervals):
|
|
72
|
-
VALID_INTERVALS = {0.25, 0.5, 1, 2, 4, 6, 8, 12}
|
|
72
|
+
VALID_INTERVALS = {0.25, 0.5, 1, 2, 4, 6, 8, 12, 24}
|
|
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):
|
|
@@ -136,7 +136,7 @@ class DailyInterval(object):
|
|
|
136
136
|
|
|
137
137
|
@interval.setter
|
|
138
138
|
def interval(self, intervals):
|
|
139
|
-
VALID_INTERVALS = {0.25, 0.5, 1, 2, 4, 6, 8, 12}
|
|
139
|
+
VALID_INTERVALS = {0.25, 0.5, 1, 2, 4, 6, 8, 12, 24}
|
|
140
140
|
|
|
141
141
|
for interval in intervals:
|
|
142
142
|
# if an hourly interval is a string, then it is a weekDay interval
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import re
|
|
3
3
|
from functools import wraps
|
|
4
|
+
from typing import Any, Container, Optional, Tuple
|
|
4
5
|
|
|
5
6
|
from tableauserverclient.datetime_helpers import parse_datetime
|
|
6
7
|
|
|
@@ -65,7 +66,7 @@ def property_is_valid_time(func):
|
|
|
65
66
|
return wrapper
|
|
66
67
|
|
|
67
68
|
|
|
68
|
-
def property_is_int(range, allowed=None):
|
|
69
|
+
def property_is_int(range: Tuple[int, int], allowed: Optional[Container[Any]] = None):
|
|
69
70
|
"""Takes a range of ints and a list of exemptions to check against
|
|
70
71
|
when setting a property on a model. The range is a tuple of (min, max) and the
|
|
71
72
|
allowed list (empty by default) allows values outside that range.
|
|
@@ -89,8 +90,10 @@ def property_is_int(range, allowed=None):
|
|
|
89
90
|
raise ValueError(error)
|
|
90
91
|
|
|
91
92
|
min, max = range
|
|
93
|
+
if value in allowed:
|
|
94
|
+
return func(self, value)
|
|
92
95
|
|
|
93
|
-
if
|
|
96
|
+
if value < min or value > max:
|
|
94
97
|
raise ValueError(error)
|
|
95
98
|
|
|
96
99
|
return func(self, value)
|
|
@@ -144,15 +147,7 @@ def property_is_data_acceleration_config(func):
|
|
|
144
147
|
def wrapper(self, value):
|
|
145
148
|
if not isinstance(value, dict):
|
|
146
149
|
raise ValueError("{} is not type 'dict', cannot update {})".format(value.__class__.__name__, func.__name__))
|
|
147
|
-
if len(value)
|
|
148
|
-
attr in value.keys()
|
|
149
|
-
for attr in (
|
|
150
|
-
"acceleration_enabled",
|
|
151
|
-
"accelerate_now",
|
|
152
|
-
"last_updated_at",
|
|
153
|
-
"acceleration_status",
|
|
154
|
-
)
|
|
155
|
-
):
|
|
150
|
+
if len(value) < 2 or not all(attr in value.keys() for attr in ("acceleration_enabled", "accelerate_now")):
|
|
156
151
|
error = "{} should have 2 keys ".format(func.__name__)
|
|
157
152
|
error += "'acceleration_enabled' and 'accelerate_now'"
|
|
158
153
|
error += "instead you have {}".format(value.keys())
|
|
@@ -31,6 +31,10 @@ class ViewItem(object):
|
|
|
31
31
|
self._workbook_id: Optional[str] = None
|
|
32
32
|
self._permissions: Optional[Callable[[], List[PermissionsRule]]] = None
|
|
33
33
|
self.tags: Set[str] = set()
|
|
34
|
+
self._data_acceleration_config = {
|
|
35
|
+
"acceleration_enabled": None,
|
|
36
|
+
"acceleration_status": None,
|
|
37
|
+
}
|
|
34
38
|
|
|
35
39
|
def __str__(self):
|
|
36
40
|
return "<ViewItem {0} '{1}' contentUrl='{2}' project={3}>".format(
|
|
@@ -133,6 +137,14 @@ class ViewItem(object):
|
|
|
133
137
|
def workbook_id(self) -> Optional[str]:
|
|
134
138
|
return self._workbook_id
|
|
135
139
|
|
|
140
|
+
@property
|
|
141
|
+
def data_acceleration_config(self):
|
|
142
|
+
return self._data_acceleration_config
|
|
143
|
+
|
|
144
|
+
@data_acceleration_config.setter
|
|
145
|
+
def data_acceleration_config(self, value):
|
|
146
|
+
self._data_acceleration_config = value
|
|
147
|
+
|
|
136
148
|
@property
|
|
137
149
|
def permissions(self) -> List[PermissionsRule]:
|
|
138
150
|
if self._permissions is None:
|
|
@@ -164,6 +176,7 @@ class ViewItem(object):
|
|
|
164
176
|
owner_elem = view_xml.find(".//t:owner", namespaces=ns)
|
|
165
177
|
project_elem = view_xml.find(".//t:project", namespaces=ns)
|
|
166
178
|
tags_elem = view_xml.find(".//t:tags", namespaces=ns)
|
|
179
|
+
data_acceleration_config_elem = view_xml.find(".//t:dataAccelerationConfig", namespaces=ns)
|
|
167
180
|
view_item._created_at = parse_datetime(view_xml.get("createdAt", None))
|
|
168
181
|
view_item._updated_at = parse_datetime(view_xml.get("updatedAt", None))
|
|
169
182
|
view_item._id = view_xml.get("id", None)
|
|
@@ -186,4 +199,25 @@ class ViewItem(object):
|
|
|
186
199
|
tags = TagItem.from_xml_element(tags_elem, ns)
|
|
187
200
|
view_item.tags = tags
|
|
188
201
|
view_item._initial_tags = copy.copy(tags)
|
|
202
|
+
if data_acceleration_config_elem is not None:
|
|
203
|
+
data_acceleration_config = parse_data_acceleration_config(data_acceleration_config_elem)
|
|
204
|
+
view_item.data_acceleration_config = data_acceleration_config
|
|
189
205
|
return view_item
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def parse_data_acceleration_config(data_acceleration_elem):
|
|
209
|
+
data_acceleration_config = dict()
|
|
210
|
+
|
|
211
|
+
acceleration_enabled = data_acceleration_elem.get("accelerationEnabled", None)
|
|
212
|
+
if acceleration_enabled is not None:
|
|
213
|
+
acceleration_enabled = string_to_bool(acceleration_enabled)
|
|
214
|
+
|
|
215
|
+
acceleration_status = data_acceleration_elem.get("accelerationStatus", None)
|
|
216
|
+
|
|
217
|
+
data_acceleration_config["acceleration_enabled"] = acceleration_enabled
|
|
218
|
+
data_acceleration_config["acceleration_status"] = acceleration_status
|
|
219
|
+
return data_acceleration_config
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def string_to_bool(s: str) -> bool:
|
|
223
|
+
return s.lower() == "true"
|
|
@@ -17,6 +17,7 @@ from .property_decorators import (
|
|
|
17
17
|
from .revision_item import RevisionItem
|
|
18
18
|
from .tag_item import TagItem
|
|
19
19
|
from .view_item import ViewItem
|
|
20
|
+
from .data_freshness_policy_item import DataFreshnessPolicyItem
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class WorkbookItem(object):
|
|
@@ -34,7 +35,7 @@ class WorkbookItem(object):
|
|
|
34
35
|
self._revisions = None
|
|
35
36
|
self._size = None
|
|
36
37
|
self._updated_at = None
|
|
37
|
-
self._views = None
|
|
38
|
+
self._views: Optional[Callable[[], List[ViewItem]]] = None
|
|
38
39
|
self.name = name
|
|
39
40
|
self._description = None
|
|
40
41
|
self.owner_id: Optional[str] = None
|
|
@@ -49,6 +50,7 @@ class WorkbookItem(object):
|
|
|
49
50
|
"last_updated_at": None,
|
|
50
51
|
"acceleration_status": None,
|
|
51
52
|
}
|
|
53
|
+
self.data_freshness_policy = None
|
|
52
54
|
self._permissions = None
|
|
53
55
|
|
|
54
56
|
return None
|
|
@@ -91,6 +93,10 @@ class WorkbookItem(object):
|
|
|
91
93
|
def description(self) -> Optional[str]:
|
|
92
94
|
return self._description
|
|
93
95
|
|
|
96
|
+
@description.setter
|
|
97
|
+
def description(self, value: str):
|
|
98
|
+
self._description = value
|
|
99
|
+
|
|
94
100
|
@property
|
|
95
101
|
def id(self) -> Optional[str]:
|
|
96
102
|
return self._id
|
|
@@ -162,6 +168,10 @@ class WorkbookItem(object):
|
|
|
162
168
|
# We had views included in a WorkbookItem response
|
|
163
169
|
return self._views
|
|
164
170
|
|
|
171
|
+
@views.setter
|
|
172
|
+
def views(self, value):
|
|
173
|
+
self._views = value
|
|
174
|
+
|
|
165
175
|
@property
|
|
166
176
|
def data_acceleration_config(self):
|
|
167
177
|
return self._data_acceleration_config
|
|
@@ -171,6 +181,15 @@ class WorkbookItem(object):
|
|
|
171
181
|
def data_acceleration_config(self, value):
|
|
172
182
|
self._data_acceleration_config = value
|
|
173
183
|
|
|
184
|
+
@property
|
|
185
|
+
def data_freshness_policy(self):
|
|
186
|
+
return self._data_freshness_policy
|
|
187
|
+
|
|
188
|
+
@data_freshness_policy.setter
|
|
189
|
+
# @property_is_data_freshness_policy
|
|
190
|
+
def data_freshness_policy(self, value):
|
|
191
|
+
self._data_freshness_policy = value
|
|
192
|
+
|
|
174
193
|
@property
|
|
175
194
|
def revisions(self) -> List[RevisionItem]:
|
|
176
195
|
if self._revisions is None:
|
|
@@ -217,8 +236,9 @@ class WorkbookItem(object):
|
|
|
217
236
|
project_name,
|
|
218
237
|
owner_id,
|
|
219
238
|
_,
|
|
220
|
-
|
|
239
|
+
views,
|
|
221
240
|
data_acceleration_config,
|
|
241
|
+
data_freshness_policy,
|
|
222
242
|
) = self._parse_element(workbook_xml, ns)
|
|
223
243
|
|
|
224
244
|
self._set_values(
|
|
@@ -235,8 +255,9 @@ class WorkbookItem(object):
|
|
|
235
255
|
project_name,
|
|
236
256
|
owner_id,
|
|
237
257
|
None,
|
|
238
|
-
|
|
258
|
+
views,
|
|
239
259
|
data_acceleration_config,
|
|
260
|
+
data_freshness_policy,
|
|
240
261
|
)
|
|
241
262
|
|
|
242
263
|
return self
|
|
@@ -258,6 +279,7 @@ class WorkbookItem(object):
|
|
|
258
279
|
tags,
|
|
259
280
|
views,
|
|
260
281
|
data_acceleration_config,
|
|
282
|
+
data_freshness_policy,
|
|
261
283
|
):
|
|
262
284
|
if id is not None:
|
|
263
285
|
self._id = id
|
|
@@ -286,10 +308,12 @@ class WorkbookItem(object):
|
|
|
286
308
|
if tags:
|
|
287
309
|
self.tags = tags
|
|
288
310
|
self._initial_tags = copy.copy(tags)
|
|
289
|
-
if views:
|
|
311
|
+
if views is not None:
|
|
290
312
|
self._views = views
|
|
291
313
|
if data_acceleration_config is not None:
|
|
292
314
|
self.data_acceleration_config = data_acceleration_config
|
|
315
|
+
if data_freshness_policy is not None:
|
|
316
|
+
self.data_freshness_policy = data_freshness_policy
|
|
293
317
|
|
|
294
318
|
@classmethod
|
|
295
319
|
def from_response(cls, resp: str, ns: Dict[str, str]) -> List["WorkbookItem"]:
|
|
@@ -356,6 +380,11 @@ class WorkbookItem(object):
|
|
|
356
380
|
if data_acceleration_elem is not None:
|
|
357
381
|
data_acceleration_config = parse_data_acceleration_config(data_acceleration_elem)
|
|
358
382
|
|
|
383
|
+
data_freshness_policy = None
|
|
384
|
+
data_freshness_policy_elem = workbook_xml.find(".//t:dataFreshnessPolicy", namespaces=ns)
|
|
385
|
+
if data_freshness_policy_elem is not None:
|
|
386
|
+
data_freshness_policy = DataFreshnessPolicyItem.from_xml_element(data_freshness_policy_elem, ns)
|
|
387
|
+
|
|
359
388
|
return (
|
|
360
389
|
id,
|
|
361
390
|
name,
|
|
@@ -372,6 +401,7 @@ class WorkbookItem(object):
|
|
|
372
401
|
tags,
|
|
373
402
|
views,
|
|
374
403
|
data_acceleration_config,
|
|
404
|
+
data_freshness_policy,
|
|
375
405
|
)
|
|
376
406
|
|
|
377
407
|
|
|
@@ -10,6 +10,7 @@ from .favorites_endpoint import Favorites
|
|
|
10
10
|
from .fileuploads_endpoint import Fileuploads
|
|
11
11
|
from .flow_runs_endpoint import FlowRuns
|
|
12
12
|
from .flows_endpoint import Flows
|
|
13
|
+
from .flow_task_endpoint import FlowTasks
|
|
13
14
|
from .groups_endpoint import Groups
|
|
14
15
|
from .jobs_endpoint import Jobs
|
|
15
16
|
from .metadata_endpoint import Metadata
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
from email.message import Message
|
|
2
2
|
import copy
|
|
3
3
|
import json
|
|
4
4
|
import io
|
|
@@ -437,14 +437,16 @@ class Datasources(QuerysetEndpoint):
|
|
|
437
437
|
url += "?includeExtract=False"
|
|
438
438
|
|
|
439
439
|
with closing(self.get_request(url, parameters={"stream": True})) as server_response:
|
|
440
|
-
|
|
440
|
+
m = Message()
|
|
441
|
+
m["Content-Disposition"] = server_response.headers["Content-Disposition"]
|
|
442
|
+
params = m.get_filename(failobj="")
|
|
441
443
|
if isinstance(filepath, io_types_w):
|
|
442
444
|
for chunk in server_response.iter_content(1024): # 1KB
|
|
443
445
|
filepath.write(chunk)
|
|
444
446
|
return_path = filepath
|
|
445
447
|
else:
|
|
446
448
|
params = fix_filename(params)
|
|
447
|
-
filename = to_filename(os.path.basename(params
|
|
449
|
+
filename = to_filename(os.path.basename(params))
|
|
448
450
|
download_path = make_download_path(filepath, filename)
|
|
449
451
|
with open(download_path, "wb") as f:
|
|
450
452
|
for chunk in server_response.iter_content(1024): # 1KB
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from threading import Thread
|
|
2
|
-
from time import sleep
|
|
3
1
|
from tableauserverclient import datetime_helpers as datetime
|
|
4
2
|
|
|
5
3
|
from packaging.version import Version
|
|
@@ -76,55 +74,20 @@ class Endpoint(object):
|
|
|
76
74
|
return parameters
|
|
77
75
|
|
|
78
76
|
def _blocking_request(self, method, url, parameters={}) -> Optional[Union["Response", Exception]]:
|
|
79
|
-
self.async_response = None
|
|
80
77
|
response = None
|
|
81
78
|
logger.debug("[{}] Begin blocking request to {}".format(datetime.timestamp(), url))
|
|
82
79
|
try:
|
|
83
80
|
response = method(url, **parameters)
|
|
84
|
-
self.async_response = response
|
|
85
81
|
logger.debug("[{}] Call finished".format(datetime.timestamp()))
|
|
86
82
|
except Exception as e:
|
|
87
83
|
logger.debug("Error making request to server: {}".format(e))
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if response and not self.async_response:
|
|
91
|
-
logger.debug("Request response not saved")
|
|
92
|
-
return None
|
|
93
|
-
logger.debug("[{}] Request complete".format(datetime.timestamp()))
|
|
94
|
-
return self.async_response
|
|
84
|
+
raise e
|
|
85
|
+
return response
|
|
95
86
|
|
|
96
87
|
def send_request_while_show_progress_threaded(
|
|
97
88
|
self, method, url, parameters={}, request_timeout=None
|
|
98
89
|
) -> Optional[Union["Response", Exception]]:
|
|
99
|
-
|
|
100
|
-
request_thread = Thread(target=self._blocking_request, args=(method, url, parameters))
|
|
101
|
-
request_thread.start()
|
|
102
|
-
except Exception as e:
|
|
103
|
-
logger.debug("Error starting server request on separate thread: {}".format(e))
|
|
104
|
-
return None
|
|
105
|
-
seconds = 0.05
|
|
106
|
-
minutes = 0
|
|
107
|
-
last_log_minute = 0
|
|
108
|
-
sleep(seconds)
|
|
109
|
-
if self.async_response is not None:
|
|
110
|
-
# a quick return for any immediate responses
|
|
111
|
-
return self.async_response
|
|
112
|
-
timed_out: bool = request_timeout is not None and seconds > request_timeout
|
|
113
|
-
while (self.async_response is None) and not timed_out:
|
|
114
|
-
sleep(DELAY_SLEEP_SECONDS)
|
|
115
|
-
seconds = seconds + DELAY_SLEEP_SECONDS
|
|
116
|
-
minutes = int(seconds / 60)
|
|
117
|
-
last_log_minute = self.log_wait_time(minutes, last_log_minute, url)
|
|
118
|
-
return self.async_response
|
|
119
|
-
|
|
120
|
-
def log_wait_time(self, minutes, last_log_minute, url) -> int:
|
|
121
|
-
logger.debug("{} Waiting....".format(datetime.timestamp()))
|
|
122
|
-
if minutes > last_log_minute: # detailed log message ~every minute
|
|
123
|
-
logger.info("[{}] Waiting ({} minutes so far) for request to {}".format(datetime.timestamp(), minutes, url))
|
|
124
|
-
last_log_minute = minutes
|
|
125
|
-
else:
|
|
126
|
-
logger.debug("[{}] Waiting for request to {}".format(datetime.timestamp(), url))
|
|
127
|
-
return last_log_minute
|
|
90
|
+
return self._blocking_request(method, url, parameters)
|
|
128
91
|
|
|
129
92
|
def _make_request(
|
|
130
93
|
self,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import List, Optional, Tuple, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from tableauserverclient.server.endpoint.endpoint import Endpoint, api
|
|
5
|
+
from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
|
|
6
|
+
from tableauserverclient.models import TaskItem, PaginationItem
|
|
7
|
+
from tableauserverclient.server import RequestFactory
|
|
8
|
+
|
|
9
|
+
from tableauserverclient.helpers.logging import logger
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from tableauserverclient.server.request_options import RequestOptions
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FlowTasks(Endpoint):
|
|
16
|
+
@property
|
|
17
|
+
def baseurl(self) -> str:
|
|
18
|
+
return "{0}/sites/{1}/tasks/flows".format(self.parent_srv.baseurl, self.parent_srv.site_id)
|
|
19
|
+
|
|
20
|
+
@api(version="3.22")
|
|
21
|
+
def create(self, flow_item: TaskItem) -> TaskItem:
|
|
22
|
+
if not flow_item:
|
|
23
|
+
error = "No flow provided"
|
|
24
|
+
raise ValueError(error)
|
|
25
|
+
logger.info("Creating an flow task %s", flow_item)
|
|
26
|
+
url = self.baseurl
|
|
27
|
+
create_req = RequestFactory.FlowTask.create_flow_task_req(flow_item)
|
|
28
|
+
server_response = self.post_request(url, create_req)
|
|
29
|
+
return server_response.content
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
from email.message import Message
|
|
2
2
|
import copy
|
|
3
3
|
import io
|
|
4
4
|
import logging
|
|
@@ -120,14 +120,16 @@ class Flows(QuerysetEndpoint):
|
|
|
120
120
|
url = "{0}/{1}/content".format(self.baseurl, flow_id)
|
|
121
121
|
|
|
122
122
|
with closing(self.get_request(url, parameters={"stream": True})) as server_response:
|
|
123
|
-
|
|
123
|
+
m = Message()
|
|
124
|
+
m["Content-Disposition"] = server_response.headers["Content-Disposition"]
|
|
125
|
+
params = m.get_filename(failobj="")
|
|
124
126
|
if isinstance(filepath, io_types_w):
|
|
125
127
|
for chunk in server_response.iter_content(1024): # 1KB
|
|
126
128
|
filepath.write(chunk)
|
|
127
129
|
return_path = filepath
|
|
128
130
|
else:
|
|
129
131
|
params = fix_filename(params)
|
|
130
|
-
filename = to_filename(os.path.basename(params
|
|
132
|
+
filename = to_filename(os.path.basename(params))
|
|
131
133
|
download_path = make_download_path(filepath, filename)
|
|
132
134
|
with open(download_path, "wb") as f:
|
|
133
135
|
for chunk in server_response.iter_content(1024): # 1KB
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
from email.message import Message
|
|
2
2
|
import copy
|
|
3
3
|
import io
|
|
4
4
|
import logging
|
|
@@ -137,7 +137,12 @@ class Workbooks(QuerysetEndpoint):
|
|
|
137
137
|
|
|
138
138
|
# Update workbook
|
|
139
139
|
@api(version="2.0")
|
|
140
|
-
|
|
140
|
+
@parameter_added_in(include_view_acceleration_status="3.22")
|
|
141
|
+
def update(
|
|
142
|
+
self,
|
|
143
|
+
workbook_item: WorkbookItem,
|
|
144
|
+
include_view_acceleration_status: bool = False,
|
|
145
|
+
) -> WorkbookItem:
|
|
141
146
|
if not workbook_item.id:
|
|
142
147
|
error = "Workbook item missing ID. Workbook must be retrieved from server first."
|
|
143
148
|
raise MissingRequiredFieldError(error)
|
|
@@ -146,6 +151,9 @@ class Workbooks(QuerysetEndpoint):
|
|
|
146
151
|
|
|
147
152
|
# Update the workbook itself
|
|
148
153
|
url = "{0}/{1}".format(self.baseurl, workbook_item.id)
|
|
154
|
+
if include_view_acceleration_status:
|
|
155
|
+
url += "?includeViewAccelerationStatus=True"
|
|
156
|
+
|
|
149
157
|
update_req = RequestFactory.Workbook.update_req(workbook_item)
|
|
150
158
|
server_response = self.put_request(url, update_req)
|
|
151
159
|
logger.info("Updated workbook item (ID: {0})".format(workbook_item.id))
|
|
@@ -483,14 +491,16 @@ class Workbooks(QuerysetEndpoint):
|
|
|
483
491
|
url += "?includeExtract=False"
|
|
484
492
|
|
|
485
493
|
with closing(self.get_request(url, parameters={"stream": True})) as server_response:
|
|
486
|
-
|
|
494
|
+
m = Message()
|
|
495
|
+
m["Content-Disposition"] = server_response.headers["Content-Disposition"]
|
|
496
|
+
params = m.get_filename(failobj="")
|
|
487
497
|
if isinstance(filepath, io_types_w):
|
|
488
498
|
for chunk in server_response.iter_content(1024): # 1KB
|
|
489
499
|
filepath.write(chunk)
|
|
490
500
|
return_path = filepath
|
|
491
501
|
else:
|
|
492
502
|
params = fix_filename(params)
|
|
493
|
-
filename = to_filename(os.path.basename(params
|
|
503
|
+
filename = to_filename(os.path.basename(params))
|
|
494
504
|
download_path = make_download_path(filepath, filename)
|
|
495
505
|
with open(download_path, "wb") as f:
|
|
496
506
|
for chunk in server_response.iter_content(1024): # 1KB
|
|
@@ -47,7 +47,11 @@ class Pager(object):
|
|
|
47
47
|
|
|
48
48
|
# Get the rest on demand as a generator
|
|
49
49
|
while self._count < last_pagination_item.total_available:
|
|
50
|
-
if
|
|
50
|
+
if (
|
|
51
|
+
len(current_item_list) == 0
|
|
52
|
+
and (last_pagination_item.page_number * last_pagination_item.page_size)
|
|
53
|
+
< last_pagination_item.total_available
|
|
54
|
+
):
|
|
51
55
|
current_item_list, last_pagination_item = self._load_next_page(last_pagination_item)
|
|
52
56
|
|
|
53
57
|
try:
|
|
@@ -57,6 +57,11 @@ def _add_hiddenview_element(views_element, view_name):
|
|
|
57
57
|
view_element.attrib["hidden"] = "true"
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
def _add_view_element(views_element, view_id):
|
|
61
|
+
view_element = ET.SubElement(views_element, "view")
|
|
62
|
+
view_element.attrib["id"] = view_id
|
|
63
|
+
|
|
64
|
+
|
|
60
65
|
def _add_credentials_element(parent_element, connection_credentials):
|
|
61
66
|
credentials_element = ET.SubElement(parent_element, "connectionCredentials")
|
|
62
67
|
if connection_credentials.password is None or connection_credentials.name is None:
|
|
@@ -911,6 +916,9 @@ class WorkbookRequest(object):
|
|
|
911
916
|
for connection in connections:
|
|
912
917
|
_add_connections_element(connections_element, connection)
|
|
913
918
|
|
|
919
|
+
if workbook_item.description is not None:
|
|
920
|
+
workbook_element.attrib["description"] = workbook_item.description
|
|
921
|
+
|
|
914
922
|
if hidden_views is not None:
|
|
915
923
|
import warnings
|
|
916
924
|
|
|
@@ -941,16 +949,61 @@ class WorkbookRequest(object):
|
|
|
941
949
|
if workbook_item.owner_id:
|
|
942
950
|
owner_element = ET.SubElement(workbook_element, "owner")
|
|
943
951
|
owner_element.attrib["id"] = workbook_item.owner_id
|
|
944
|
-
if workbook_item.
|
|
952
|
+
if workbook_item._views is not None:
|
|
953
|
+
views_element = ET.SubElement(workbook_element, "views")
|
|
954
|
+
for view in workbook_item.views:
|
|
955
|
+
_add_view_element(views_element, view.id)
|
|
956
|
+
if workbook_item.data_acceleration_config:
|
|
945
957
|
data_acceleration_config = workbook_item.data_acceleration_config
|
|
946
958
|
data_acceleration_element = ET.SubElement(workbook_element, "dataAccelerationConfig")
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
959
|
+
if data_acceleration_config["acceleration_enabled"] is not None:
|
|
960
|
+
data_acceleration_element.attrib["accelerationEnabled"] = str(
|
|
961
|
+
data_acceleration_config["acceleration_enabled"]
|
|
962
|
+
).lower()
|
|
950
963
|
if data_acceleration_config["accelerate_now"] is not None:
|
|
951
964
|
data_acceleration_element.attrib["accelerateNow"] = str(
|
|
952
965
|
data_acceleration_config["accelerate_now"]
|
|
953
966
|
).lower()
|
|
967
|
+
if workbook_item.data_freshness_policy is not None:
|
|
968
|
+
data_freshness_policy_config = workbook_item.data_freshness_policy
|
|
969
|
+
data_freshness_policy_element = ET.SubElement(workbook_element, "dataFreshnessPolicy")
|
|
970
|
+
data_freshness_policy_element.attrib["option"] = str(data_freshness_policy_config.option)
|
|
971
|
+
# Fresh Every Schedule
|
|
972
|
+
if data_freshness_policy_config.option == "FreshEvery":
|
|
973
|
+
if data_freshness_policy_config.fresh_every_schedule is not None:
|
|
974
|
+
fresh_every_element = ET.SubElement(data_freshness_policy_element, "freshEverySchedule")
|
|
975
|
+
fresh_every_element.attrib[
|
|
976
|
+
"frequency"
|
|
977
|
+
] = data_freshness_policy_config.fresh_every_schedule.frequency
|
|
978
|
+
fresh_every_element.attrib["value"] = str(data_freshness_policy_config.fresh_every_schedule.value)
|
|
979
|
+
else:
|
|
980
|
+
raise ValueError(f"data_freshness_policy_config.fresh_every_schedule must be populated.")
|
|
981
|
+
# Fresh At Schedule
|
|
982
|
+
if data_freshness_policy_config.option == "FreshAt":
|
|
983
|
+
if data_freshness_policy_config.fresh_at_schedule is not None:
|
|
984
|
+
fresh_at_element = ET.SubElement(data_freshness_policy_element, "freshAtSchedule")
|
|
985
|
+
frequency = data_freshness_policy_config.fresh_at_schedule.frequency
|
|
986
|
+
fresh_at_element.attrib["frequency"] = frequency
|
|
987
|
+
fresh_at_element.attrib["time"] = str(data_freshness_policy_config.fresh_at_schedule.time)
|
|
988
|
+
fresh_at_element.attrib["timezone"] = str(data_freshness_policy_config.fresh_at_schedule.timezone)
|
|
989
|
+
intervals = data_freshness_policy_config.fresh_at_schedule.interval_item
|
|
990
|
+
# Fresh At Schedule intervals if Frequency is Week or Month
|
|
991
|
+
if frequency != DataFreshnessPolicyItem.FreshAt.Frequency.Day:
|
|
992
|
+
if intervals is not None:
|
|
993
|
+
# if intervals is not None or frequency != DataFreshnessPolicyItem.FreshAt.Frequency.Day:
|
|
994
|
+
intervals_element = ET.SubElement(fresh_at_element, "intervals")
|
|
995
|
+
for interval in intervals:
|
|
996
|
+
expression = IntervalItem.Occurrence.WeekDay
|
|
997
|
+
if frequency == DataFreshnessPolicyItem.FreshAt.Frequency.Month:
|
|
998
|
+
expression = IntervalItem.Occurrence.MonthDay
|
|
999
|
+
single_interval_element = ET.SubElement(intervals_element, "interval")
|
|
1000
|
+
single_interval_element.attrib[expression] = interval
|
|
1001
|
+
else:
|
|
1002
|
+
raise ValueError(
|
|
1003
|
+
f"fresh_at_schedule.interval_item must be populated for " f"Week & Month frequency."
|
|
1004
|
+
)
|
|
1005
|
+
else:
|
|
1006
|
+
raise ValueError(f"data_freshness_policy_config.fresh_at_schedule must be populated.")
|
|
954
1007
|
|
|
955
1008
|
return ET.tostring(xml_request)
|
|
956
1009
|
|
|
@@ -1061,6 +1114,40 @@ class TaskRequest(object):
|
|
|
1061
1114
|
return ET.tostring(xml_request)
|
|
1062
1115
|
|
|
1063
1116
|
|
|
1117
|
+
class FlowTaskRequest(object):
|
|
1118
|
+
@_tsrequest_wrapped
|
|
1119
|
+
def create_flow_task_req(self, xml_request: ET.Element, flow_item: "TaskItem") -> bytes:
|
|
1120
|
+
flow_element = ET.SubElement(xml_request, "runFlow")
|
|
1121
|
+
|
|
1122
|
+
# Main attributes
|
|
1123
|
+
flow_element.attrib["type"] = flow_item.task_type
|
|
1124
|
+
|
|
1125
|
+
if flow_item.target is not None:
|
|
1126
|
+
target_element = ET.SubElement(flow_element, flow_item.target.type)
|
|
1127
|
+
target_element.attrib["id"] = flow_item.target.id
|
|
1128
|
+
|
|
1129
|
+
if flow_item.schedule_item is None:
|
|
1130
|
+
return ET.tostring(xml_request)
|
|
1131
|
+
|
|
1132
|
+
# Schedule attributes
|
|
1133
|
+
schedule_element = ET.SubElement(xml_request, "schedule")
|
|
1134
|
+
|
|
1135
|
+
interval_item = flow_item.schedule_item.interval_item
|
|
1136
|
+
schedule_element.attrib["frequency"] = interval_item._frequency
|
|
1137
|
+
frequency_element = ET.SubElement(schedule_element, "frequencyDetails")
|
|
1138
|
+
frequency_element.attrib["start"] = str(interval_item.start_time)
|
|
1139
|
+
if hasattr(interval_item, "end_time") and interval_item.end_time is not None:
|
|
1140
|
+
frequency_element.attrib["end"] = str(interval_item.end_time)
|
|
1141
|
+
if hasattr(interval_item, "interval") and interval_item.interval:
|
|
1142
|
+
intervals_element = ET.SubElement(frequency_element, "intervals")
|
|
1143
|
+
for interval in interval_item._interval_type_pairs(): # type: ignore
|
|
1144
|
+
expression, value = interval
|
|
1145
|
+
single_interval_element = ET.SubElement(intervals_element, "interval")
|
|
1146
|
+
single_interval_element.attrib[expression] = value
|
|
1147
|
+
|
|
1148
|
+
return ET.tostring(xml_request)
|
|
1149
|
+
|
|
1150
|
+
|
|
1064
1151
|
class SubscriptionRequest(object):
|
|
1065
1152
|
@_tsrequest_wrapped
|
|
1066
1153
|
def create_req(self, xml_request: ET.Element, subscription_item: "SubscriptionItem") -> bytes:
|
|
@@ -1200,6 +1287,7 @@ class RequestFactory(object):
|
|
|
1200
1287
|
Favorite = FavoriteRequest()
|
|
1201
1288
|
Fileupload = FileuploadRequest()
|
|
1202
1289
|
Flow = FlowRequest()
|
|
1290
|
+
FlowTask = FlowTaskRequest()
|
|
1203
1291
|
Group = GroupRequest()
|
|
1204
1292
|
Metric = MetricRequest()
|
|
1205
1293
|
Permission = PermissionRequest()
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from typing_extensions import Self
|
|
4
|
+
|
|
1
5
|
from tableauserverclient.models.property_decorators import property_is_int
|
|
2
6
|
import logging
|
|
3
7
|
|
|
@@ -152,17 +156,27 @@ class _FilterOptionsBase(RequestOptionsBase):
|
|
|
152
156
|
|
|
153
157
|
def __init__(self):
|
|
154
158
|
self.view_filters = []
|
|
159
|
+
self.view_parameters = []
|
|
155
160
|
|
|
156
161
|
def get_query_params(self):
|
|
157
162
|
raise NotImplementedError()
|
|
158
163
|
|
|
159
|
-
def vf(self, name, value):
|
|
164
|
+
def vf(self, name: str, value: str) -> Self:
|
|
165
|
+
"""Apply a filter to the view for a filter that is a normal column
|
|
166
|
+
within the view."""
|
|
160
167
|
self.view_filters.append((name, value))
|
|
161
168
|
return self
|
|
162
169
|
|
|
163
|
-
def
|
|
170
|
+
def parameter(self, name: str, value: str) -> Self:
|
|
171
|
+
"""Apply a filter based on a parameter within the workbook."""
|
|
172
|
+
self.view_parameters.append((name, value))
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
def _append_view_filters(self, params) -> None:
|
|
164
176
|
for name, value in self.view_filters:
|
|
165
177
|
params["vf_" + name] = value
|
|
178
|
+
for name, value in self.view_parameters:
|
|
179
|
+
params[name] = value
|
|
166
180
|
|
|
167
181
|
|
|
168
182
|
class CSVRequestOptions(_FilterOptionsBase):
|
|
@@ -261,11 +275,13 @@ class PDFRequestOptions(_FilterOptionsBase):
|
|
|
261
275
|
Portrait = "portrait"
|
|
262
276
|
Landscape = "landscape"
|
|
263
277
|
|
|
264
|
-
def __init__(self, page_type=None, orientation=None, maxage=-1):
|
|
278
|
+
def __init__(self, page_type=None, orientation=None, maxage=-1, viz_height=None, viz_width=None):
|
|
265
279
|
super(PDFRequestOptions, self).__init__()
|
|
266
280
|
self.page_type = page_type
|
|
267
281
|
self.orientation = orientation
|
|
268
282
|
self.max_age = maxage
|
|
283
|
+
self.viz_height = viz_height
|
|
284
|
+
self.viz_width = viz_width
|
|
269
285
|
|
|
270
286
|
@property
|
|
271
287
|
def max_age(self):
|
|
@@ -276,6 +292,24 @@ class PDFRequestOptions(_FilterOptionsBase):
|
|
|
276
292
|
def max_age(self, value):
|
|
277
293
|
self._max_age = value
|
|
278
294
|
|
|
295
|
+
@property
|
|
296
|
+
def viz_height(self):
|
|
297
|
+
return self._viz_height
|
|
298
|
+
|
|
299
|
+
@viz_height.setter
|
|
300
|
+
@property_is_int(range=(0, sys.maxsize), allowed=(None,))
|
|
301
|
+
def viz_height(self, value):
|
|
302
|
+
self._viz_height = value
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def viz_width(self):
|
|
306
|
+
return self._viz_width
|
|
307
|
+
|
|
308
|
+
@viz_width.setter
|
|
309
|
+
@property_is_int(range=(0, sys.maxsize), allowed=(None,))
|
|
310
|
+
def viz_width(self, value):
|
|
311
|
+
self._viz_width = value
|
|
312
|
+
|
|
279
313
|
def get_query_params(self):
|
|
280
314
|
params = {}
|
|
281
315
|
if self.page_type:
|
|
@@ -287,6 +321,16 @@ class PDFRequestOptions(_FilterOptionsBase):
|
|
|
287
321
|
if self.max_age != -1:
|
|
288
322
|
params["maxAge"] = self.max_age
|
|
289
323
|
|
|
324
|
+
# XOR. Either both are None or both are not None.
|
|
325
|
+
if (self.viz_height is None) ^ (self.viz_width is None):
|
|
326
|
+
raise ValueError("viz_height and viz_width must be specified together")
|
|
327
|
+
|
|
328
|
+
if self.viz_height is not None:
|
|
329
|
+
params["vizHeight"] = self.viz_height
|
|
330
|
+
|
|
331
|
+
if self.viz_width is not None:
|
|
332
|
+
params["vizWidth"] = self.viz_width
|
|
333
|
+
|
|
290
334
|
self._append_view_filters(params)
|
|
291
335
|
|
|
292
336
|
return params
|
|
@@ -25,6 +25,7 @@ from .endpoint import (
|
|
|
25
25
|
Databases,
|
|
26
26
|
Tables,
|
|
27
27
|
Flows,
|
|
28
|
+
FlowTasks,
|
|
28
29
|
Webhooks,
|
|
29
30
|
DataAccelerationReport,
|
|
30
31
|
Favorites,
|
|
@@ -82,6 +83,7 @@ class Server(object):
|
|
|
82
83
|
self.datasources = Datasources(self)
|
|
83
84
|
self.favorites = Favorites(self)
|
|
84
85
|
self.flows = Flows(self)
|
|
86
|
+
self.flow_tasks = FlowTasks(self)
|
|
85
87
|
self.projects = Projects(self)
|
|
86
88
|
self.schedules = Schedules(self)
|
|
87
89
|
self.server_info = ServerInfo(self)
|
{tableauserverclient-0.29.dist-info → tableauserverclient-0.30.post0.dev1.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tableauserverclient
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.30.post0.dev1
|
|
4
4
|
Summary: A Python module for working with the Tableau Server REST API.
|
|
5
5
|
Author-email: Tableau <github@tableau.com>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -41,6 +41,7 @@ Requires-Dist: defusedxml >=0.7.1
|
|
|
41
41
|
Requires-Dist: packaging >=23.1
|
|
42
42
|
Requires-Dist: requests >=2.31
|
|
43
43
|
Requires-Dist: urllib3 ==2.0.7
|
|
44
|
+
Requires-Dist: typing-extensions >=4.0.1
|
|
44
45
|
Provides-Extra: test
|
|
45
46
|
Requires-Dist: argparse ; extra == 'test'
|
|
46
47
|
Requires-Dist: black ==23.7 ; extra == 'test'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
tableauserverclient/__init__.py,sha256=
|
|
2
|
-
tableauserverclient/_version.py,sha256=
|
|
1
|
+
tableauserverclient/__init__.py,sha256=3pYu_AmAPRRuMREy0Uq5Dk9MTFX9tIKa4Vc-j7q8iLI,1125
|
|
2
|
+
tableauserverclient/_version.py,sha256=jhLMneVfFrXy4GLLWUM8iJQL5D7Ngdn0QaWQhKzlIKA,507
|
|
3
3
|
tableauserverclient/config.py,sha256=frumJjDxOQug_R54kjrnHLyaIgg1bV7aUoNPHMJlBhc,430
|
|
4
4
|
tableauserverclient/datetime_helpers.py,sha256=_-gWz5I2_KHT5AzW_boD8meH6loTTKdK-_62h1MA6ko,884
|
|
5
5
|
tableauserverclient/exponential_backoff.py,sha256=HtAfbbVnYtiOe2_ZzKqZmsml40EDZBaC3bttif5qeG8,1474
|
|
@@ -10,13 +10,14 @@ tableauserverclient/helpers/__init__.py,sha256=llpqF9zV5dsP5hQt8dyop33Op5OzGmxW0
|
|
|
10
10
|
tableauserverclient/helpers/headers.py,sha256=Pg-ZWSgV2Yp813BV1Tzo8F2Hn4P01zscla2RRM4yqNk,411
|
|
11
11
|
tableauserverclient/helpers/logging.py,sha256=5q_oR3wERHVXFXY6XDnLYML5-HdAPJTmqhH9IZg4VfY,113
|
|
12
12
|
tableauserverclient/helpers/strings.py,sha256=HRTP31HIdD3gFxDDHuBsmOl_8cLdh8fa3xLALgYsUAQ,1563
|
|
13
|
-
tableauserverclient/models/__init__.py,sha256=
|
|
13
|
+
tableauserverclient/models/__init__.py,sha256=odiSv68Ct4G93dSKuhaq3dK0PuL2lF3JGrYedaaLJ1s,1684
|
|
14
14
|
tableauserverclient/models/column_item.py,sha256=gV2r7xOVmcn9UnMXFOe9_ORZ85oha1KtIH-crKUhgN4,1972
|
|
15
15
|
tableauserverclient/models/connection_credentials.py,sha256=8Wa8B_-cB3INFUup-Pyd5znsUD5opgYYH5TfipCzKTU,1559
|
|
16
16
|
tableauserverclient/models/connection_item.py,sha256=BiLrjWY42EtkAyh2_3juo31rYW9YZN1e3uOXMXIaslY,4743
|
|
17
17
|
tableauserverclient/models/custom_view_item.py,sha256=Gr2lYgBU1zUfCoaXHhqTzWDspmL33UqOxlt48Y8yaGg,5782
|
|
18
18
|
tableauserverclient/models/data_acceleration_report_item.py,sha256=5Z18-fppR_lnAyPKHwxi4gOP6kqcYX3Q3RdwiD4Zy94,3402
|
|
19
19
|
tableauserverclient/models/data_alert_item.py,sha256=sJpncsKY1BY-tFJKDpX7xaHy6aGvELWrorsILJxr3lI,6458
|
|
20
|
+
tableauserverclient/models/data_freshness_policy_item.py,sha256=p5iyGHl-juDM3ARl9tWr3i_LnpzalxUTRoDBP6FdTMo,7639
|
|
20
21
|
tableauserverclient/models/database_item.py,sha256=SAwoxsgeowwFN20zGt3-226kJabk0M2tqbVLQthvmRs,8043
|
|
21
22
|
tableauserverclient/models/datasource_item.py,sha256=ZZZvYfzg6gD7DlZWzfEJ3VBQBMYADJ9GeoWV02F7Z8E,11944
|
|
22
23
|
tableauserverclient/models/dqw_item.py,sha256=MFpxSP6vSzpjh5GLNrom--Sbi8iLwuGLuAda8KHiIa8,3718
|
|
@@ -26,16 +27,16 @@ tableauserverclient/models/fileupload_item.py,sha256=tx3LfgRxVqeHGpCN6kTDPkdBY5i
|
|
|
26
27
|
tableauserverclient/models/flow_item.py,sha256=rPIVDe8_tHied8TDRiGpg2gyTyQWlNiGb3vGhqF9YJw,7320
|
|
27
28
|
tableauserverclient/models/flow_run_item.py,sha256=pwJNz0m7l--Sb6z0o83ebWfAmTVFYSr-0apMxHWt0Vs,3139
|
|
28
29
|
tableauserverclient/models/group_item.py,sha256=1KP_KmIG1veJJGyte82aFvMAiU_tvzYtaP2vLEReLoo,3621
|
|
29
|
-
tableauserverclient/models/interval_item.py,sha256=
|
|
30
|
+
tableauserverclient/models/interval_item.py,sha256=NZbGNuPmFI6NhEZWZVAlWfIFbsnIYbcvxvvv9yvgfV8,9057
|
|
30
31
|
tableauserverclient/models/job_item.py,sha256=PMt7G2qu8WAxe-b3f_0BtQ9JcCSvk-g5L5EcOUpYFCY,8619
|
|
31
32
|
tableauserverclient/models/metric_item.py,sha256=Zrzi_Un43p2jzE_4OhetxhjfeSUYzotMfm8Hjd5WOhI,5397
|
|
32
33
|
tableauserverclient/models/pagination_item.py,sha256=Hdr2EIKWkXfrjWOr-Ao_vzQTWB2akUfxxtaupFKLrlQ,1432
|
|
33
34
|
tableauserverclient/models/permissions_item.py,sha256=2WlJOc1Vxkxl-WnAkFaacWsGxUXU9_d85AetSKrKwP0,3731
|
|
34
35
|
tableauserverclient/models/project_item.py,sha256=DNxAhkMkRt9MLcMie4w8nuTG4sApIkt618onBEdoM_c,6915
|
|
35
|
-
tableauserverclient/models/property_decorators.py,sha256=
|
|
36
|
+
tableauserverclient/models/property_decorators.py,sha256=W5EtN7bqDHrpLQF63i1cGOhPUJi0RzMJ7qmIlJVrGuE,4711
|
|
36
37
|
tableauserverclient/models/reference_item.py,sha256=HddG1loGNJU7gB7hBccCjnhagnSEbk4_wY2kuPbNhRQ,534
|
|
37
38
|
tableauserverclient/models/revision_item.py,sha256=fLkcScAgBBiVMCyDPk4aboyZNxrykIavzJP2vkn6Rq0,2787
|
|
38
|
-
tableauserverclient/models/schedule_item.py,sha256=
|
|
39
|
+
tableauserverclient/models/schedule_item.py,sha256=Tr4uANbrE5X6gQWiN7VR3MzTYTmKQktgKTOuk3hoKeU,11350
|
|
39
40
|
tableauserverclient/models/server_info_item.py,sha256=pR_fqqPuiBwpLn0wr4rQ4vxK7gxGJVTtaswzwjslheE,1880
|
|
40
41
|
tableauserverclient/models/site_item.py,sha256=xvXpsR0eEz2ypS-94KJTMS-gusHV1bLomKIhWX7GXrU,42256
|
|
41
42
|
tableauserverclient/models/subscription_item.py,sha256=XkQiAi_gWPBnxtTaB8TgrFa-IDVDtai9H00Ilay0T64,4744
|
|
@@ -44,35 +45,36 @@ tableauserverclient/models/tableau_auth.py,sha256=TdkOtRnNRi4p0w_WhBsWqUunNVG9nx
|
|
|
44
45
|
tableauserverclient/models/tableau_types.py,sha256=1iq1034K1W9ugLAxON_t8n2UxSouiWV_ZEwoHiNM94c,918
|
|
45
46
|
tableauserverclient/models/tag_item.py,sha256=mVfJGZG8PspPM8hBxX38heEQ_Rnp5sSwfwWdLNA-UV0,619
|
|
46
47
|
tableauserverclient/models/target.py,sha256=dVc1SHwo9ZASR8250MhAPODXX_G5UqA4BnK8hykRE6E,263
|
|
47
|
-
tableauserverclient/models/task_item.py,sha256=
|
|
48
|
+
tableauserverclient/models/task_item.py,sha256=EHVJo6NyORC_NZ0NFZTt8ylRBCeO6LFxI1Ubf15L2zo,3775
|
|
48
49
|
tableauserverclient/models/user_item.py,sha256=vaQD3nvU7bJVD6t0nkWQFse8UDz8HTp8wh_6WmCjKtM,15776
|
|
49
|
-
tableauserverclient/models/view_item.py,sha256=
|
|
50
|
+
tableauserverclient/models/view_item.py,sha256=OcZqrCJt0cjz1bSUUdLIseOYeDfo4ji4UWkv11el6Bc,8106
|
|
50
51
|
tableauserverclient/models/webhook_item.py,sha256=LLf2HQ2Y50Rw-c4dSYyv6MXLbXdij9Iyy261gu9zpv4,2650
|
|
51
|
-
tableauserverclient/models/workbook_item.py,sha256=
|
|
52
|
+
tableauserverclient/models/workbook_item.py,sha256=w6RVKOPFJ9NEmz7OG0SuKw_8FrTE4rWLKw4JkHl1lag,14118
|
|
52
53
|
tableauserverclient/server/__init__.py,sha256=ZyvucBvBQMPyTVW7oDHl4W9NWuaSVSMfN9cTcSQRmKw,406
|
|
53
54
|
tableauserverclient/server/exceptions.py,sha256=l-O4lcoEeXJC7fLWUanIcZM_Mf8m54uK3NQuKNj3RCg,183
|
|
54
55
|
tableauserverclient/server/filter.py,sha256=WZ_dIBuADRZf8VTYcRgn1rulSGUOJq8_uUltq05KkRI,1119
|
|
55
|
-
tableauserverclient/server/pager.py,sha256=
|
|
56
|
+
tableauserverclient/server/pager.py,sha256=JLud4Q6tyzfcoIQ5ykSL_veTuvQag-NDNfrPvoC3Bd0,2900
|
|
56
57
|
tableauserverclient/server/query.py,sha256=qMweo-N5P_V9z2Or4u_QrStaBbGL6HOg4Y4EDg8uh44,5735
|
|
57
|
-
tableauserverclient/server/request_factory.py,sha256=
|
|
58
|
-
tableauserverclient/server/request_options.py,sha256=
|
|
59
|
-
tableauserverclient/server/server.py,sha256=
|
|
58
|
+
tableauserverclient/server/request_factory.py,sha256=DnZt4kYUtOmWsZxjitvxVnslTaufOapuD5iLsxWUuyc,64606
|
|
59
|
+
tableauserverclient/server/request_options.py,sha256=_eWeWZH9dIv0nPU2hCVjL99kIV56H2q8cm22a088iiM,9982
|
|
60
|
+
tableauserverclient/server/server.py,sha256=8XOze5vSEVY9Nl0WOeyYljbg2i4Yp-_znlKepCvm3Es,8458
|
|
60
61
|
tableauserverclient/server/sort.py,sha256=ikndqXW2r2FeacJzybC2TVcJGn4ktviWgXwPyK-AX-0,208
|
|
61
|
-
tableauserverclient/server/endpoint/__init__.py,sha256=
|
|
62
|
+
tableauserverclient/server/endpoint/__init__.py,sha256=GcWWEKuMPdqHY2OtYYpc8LF3qkMq3m65k9DhTSqenu8,1183
|
|
62
63
|
tableauserverclient/server/endpoint/auth_endpoint.py,sha256=EOGGlGHWUZRN0SFBlYnEWUSwCH1lrB0Zfv4VWdd7HQs,5139
|
|
63
64
|
tableauserverclient/server/endpoint/custom_views_endpoint.py,sha256=SShsI8TnDe3tPXCrCsjs3Oy5Ym4j-QrcDXUfZ7rF9Y4,4542
|
|
64
65
|
tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py,sha256=nfC2gXoCke-YXGTiMWGDRBK_lDlLa9Y6hts7w3Zs8cI,1170
|
|
65
66
|
tableauserverclient/server/endpoint/data_alert_endpoint.py,sha256=c6-ICGRVDRQN7ClAgCeJsk6JK4HyBvtQGQVLOzNc4eo,5358
|
|
66
67
|
tableauserverclient/server/endpoint/databases_endpoint.py,sha256=0V6cHwF5E1K1oQ9JKT-rb7t1EFPNwlSKMC2HyqEqYPw,5366
|
|
67
|
-
tableauserverclient/server/endpoint/datasources_endpoint.py,sha256=
|
|
68
|
+
tableauserverclient/server/endpoint/datasources_endpoint.py,sha256=6EvZc-mrOZo_qlFORBPVRzWF4srDEBdmFgw-0osxbH8,20489
|
|
68
69
|
tableauserverclient/server/endpoint/default_permissions_endpoint.py,sha256=HQSYPIJwVCimuWo4YGYvsc41_-rweLHmJ-tQrWdH-Y8,4386
|
|
69
70
|
tableauserverclient/server/endpoint/dqw_endpoint.py,sha256=TvzjonvIiGtp4EisoBKms1lHOkVTjagQACw6unyY9_8,2418
|
|
70
|
-
tableauserverclient/server/endpoint/endpoint.py,sha256=
|
|
71
|
+
tableauserverclient/server/endpoint/endpoint.py,sha256=8-XteqZmfuHDyqR_mGSXKZm9esWbeybaZByHEZoBEac,12743
|
|
71
72
|
tableauserverclient/server/endpoint/exceptions.py,sha256=fh4fFYasqiKd3EPTxriVsmLkRL24wz9UvX94BMRfyLY,2534
|
|
72
73
|
tableauserverclient/server/endpoint/favorites_endpoint.py,sha256=D0MCco9kxaeCShfWOnVt3UpttJRgczJmJKXb3w0jpw4,6701
|
|
73
74
|
tableauserverclient/server/endpoint/fileuploads_endpoint.py,sha256=fh2xH6a8mMcBI14QzodWPBMabK1_9SMDbzZbawz-ZJc,2553
|
|
74
75
|
tableauserverclient/server/endpoint/flow_runs_endpoint.py,sha256=1vwkOrb574natWl-njFOij8xaV32e9RU3-rFbu4bML0,3338
|
|
75
|
-
tableauserverclient/server/endpoint/
|
|
76
|
+
tableauserverclient/server/endpoint/flow_task_endpoint.py,sha256=j3snbRiYAPkI3FwhGG-CxKogHXP3SY59ep74YPrutpw,1113
|
|
77
|
+
tableauserverclient/server/endpoint/flows_endpoint.py,sha256=cVboeR4IZa0Ck8OvYHkzpNzUJTvYgu7TzmF4Fd6bP20,13088
|
|
76
78
|
tableauserverclient/server/endpoint/groups_endpoint.py,sha256=_aSTVy7m3NXSEqgC0cPfJt6KOaPoWprxzz0MD8CsiiY,6640
|
|
77
79
|
tableauserverclient/server/endpoint/jobs_endpoint.py,sha256=P5Hdv1hsNVxd5KdrcNCfR9ZtJ7v3UgrfATCBItVdAxY,3239
|
|
78
80
|
tableauserverclient/server/endpoint/metadata_endpoint.py,sha256=AKnmRNj9U85p__igiuY_vhhipsxNXpQ_9zzNUtTuU9o,5203
|
|
@@ -89,10 +91,10 @@ tableauserverclient/server/endpoint/tasks_endpoint.py,sha256=HjtSGXBGS8Eriaf5bSW
|
|
|
89
91
|
tableauserverclient/server/endpoint/users_endpoint.py,sha256=QRoApMyP53nF-AqvC7oLHNcT4VJE8s5ykjUnI5ojmPY,7274
|
|
90
92
|
tableauserverclient/server/endpoint/views_endpoint.py,sha256=GIYPftL5B0YUOb_SisFtfLP0LPIAbbUv24XCi15UH_M,7118
|
|
91
93
|
tableauserverclient/server/endpoint/webhooks_endpoint.py,sha256=-HsbAuKECNcx5EZkqp097PfGHLCNwPRnxbrdzreTpnM,2835
|
|
92
|
-
tableauserverclient/server/endpoint/workbooks_endpoint.py,sha256=
|
|
93
|
-
tableauserverclient-0.
|
|
94
|
-
tableauserverclient-0.
|
|
95
|
-
tableauserverclient-0.
|
|
96
|
-
tableauserverclient-0.
|
|
97
|
-
tableauserverclient-0.
|
|
98
|
-
tableauserverclient-0.
|
|
94
|
+
tableauserverclient/server/endpoint/workbooks_endpoint.py,sha256=xWG8DJQFHLsp0DoUgE-sNdjTAy-aVFBfYASRizhdV88,22208
|
|
95
|
+
tableauserverclient-0.30.post0.dev1.dist-info/LICENSE,sha256=MMkY7MguOb4L-WCmmqrVcwg2iwSGaz-RfAMFvXRXwsQ,1074
|
|
96
|
+
tableauserverclient-0.30.post0.dev1.dist-info/LICENSE.versioneer,sha256=OYaGozOXk7bUNSm1z577iDYpti8a40XIqqR4lyQ47D8,334
|
|
97
|
+
tableauserverclient-0.30.post0.dev1.dist-info/METADATA,sha256=3MUzWOn6XvZdbPd9-0Z-zKUVKvgIXdrl-QxI6HQ8RyA,4125
|
|
98
|
+
tableauserverclient-0.30.post0.dev1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
99
|
+
tableauserverclient-0.30.post0.dev1.dist-info/top_level.txt,sha256=kBnL39G2RlGqxJSsShDBWG4WZ3NFcWJkv1EGiOfZh4Q,20
|
|
100
|
+
tableauserverclient-0.30.post0.dev1.dist-info/RECORD,,
|
{tableauserverclient-0.29.dist-info → tableauserverclient-0.30.post0.dev1.dist-info}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
{tableauserverclient-0.29.dist-info → tableauserverclient-0.30.post0.dev1.dist-info}/top_level.txt
RENAMED
|
File without changes
|