tableauserverclient 0.28__py3-none-any.whl → 0.30__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.
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2023-10-04T00:33:03-0700",
11
+ "date": "2024-01-19T11:30:24-0800",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "72eb3c8500193e4f20defa20c8a6f8bbf34b2f43",
15
- "version": "0.28"
14
+ "full-revisionid": "f84d7d5302344f0a4ac369c501b122931ae7e79e",
15
+ "version": "0.30"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -7,7 +7,7 @@ BYTES_PER_MB = 1024 * 1024
7
7
  # For when a datasource is over 64MB, break it into 5MB(standard chunk size) chunks
8
8
  CHUNK_SIZE_MB = 5 * 10 # 5MB felt too slow, upped it to 50
9
9
 
10
- DELAY_SLEEP_SECONDS = 10
10
+ DELAY_SLEEP_SECONDS = 0.1
11
11
 
12
12
  # The maximum size of a file that can be published in a single request is 64MB
13
13
  FILESIZE_LIMIT_MB = 64
@@ -0,0 +1,17 @@
1
+ from copy import deepcopy
2
+ from urllib.parse import unquote_plus
3
+
4
+
5
+ def fix_filename(params):
6
+ if "filename*" not in params:
7
+ return params
8
+
9
+ params = deepcopy(params)
10
+ filename = params["filename*"]
11
+ prefix = "UTF-8''"
12
+ if filename.startswith(prefix):
13
+ filename = filename[len(prefix) :]
14
+
15
+ params["filename"] = unquote_plus(filename)
16
+ del params["filename*"]
17
+ return params
@@ -9,8 +9,6 @@ from typing import TypeVar
9
9
  T = TypeVar("T", str, bytes)
10
10
 
11
11
 
12
- # usage: _redact_any_type("<xml workbook password= cooliothesecond>")
13
- # -> b"<xml workbook password =***************">
14
12
  def _redact_any_type(xml: T, sensitive_word: T, replacement: T, encoding=None) -> T:
15
13
  try:
16
14
  root = fromstring(xml)
@@ -29,7 +29,12 @@ class HourlyInterval(object):
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
32
- self.interval = interval_value
32
+
33
+ # interval should be a tuple, if it is not, assign as a tuple with single value
34
+ if isinstance(interval_value, tuple):
35
+ self.interval = interval_value
36
+ else:
37
+ self.interval = (interval_value,)
33
38
 
34
39
  def __repr__(self):
35
40
  return f"<{self.__class__.__name__} start={self.start_time} end={self.end_time} interval={self.interval}>"
@@ -63,25 +68,44 @@ class HourlyInterval(object):
63
68
  return self._interval
64
69
 
65
70
  @interval.setter
66
- def interval(self, interval):
67
- VALID_INTERVALS = {0.25, 0.5, 1, 2, 4, 6, 8, 12}
68
- if float(interval) not in VALID_INTERVALS:
69
- error = "Invalid interval {} not in {}".format(interval, str(VALID_INTERVALS))
70
- raise ValueError(error)
71
+ def interval(self, intervals):
72
+ VALID_INTERVALS = {0.25, 0.5, 1, 2, 4, 6, 8, 12, 24}
73
+ for interval in intervals:
74
+ # if an hourly interval is a string, then it is a weekDay interval
75
+ if isinstance(interval, str) and not interval.isnumeric() and not hasattr(IntervalItem.Day, interval):
76
+ error = "Invalid weekDay interval {}".format(interval)
77
+ raise ValueError(error)
71
78
 
72
- self._interval = interval
79
+ # if an hourly interval is a number, it is an hours or minutes interval
80
+ if isinstance(interval, (int, float)) and float(interval) not in VALID_INTERVALS:
81
+ error = "Invalid interval {} not in {}".format(interval, str(VALID_INTERVALS))
82
+ raise ValueError(error)
83
+
84
+ self._interval = intervals
73
85
 
74
86
  def _interval_type_pairs(self):
75
- # We use fractional hours for the two minute-based intervals.
76
- # Need to convert to minutes from hours here
77
- if self.interval in {0.25, 0.5}:
78
- calculated_interval = int(self.interval * 60)
79
- interval_type = IntervalItem.Occurrence.Minutes
80
- else:
81
- calculated_interval = self.interval
82
- interval_type = IntervalItem.Occurrence.Hours
87
+ interval_type_pairs = []
88
+ for interval in self.interval:
89
+ # We use fractional hours for the two minute-based intervals.
90
+ # Need to convert to minutes from hours here
91
+ if interval in {0.25, 0.5}:
92
+ calculated_interval = int(interval * 60)
93
+ interval_type = IntervalItem.Occurrence.Minutes
94
+
95
+ interval_type_pairs.append((interval_type, str(calculated_interval)))
96
+ else:
97
+ # if the interval is a non-numeric string, it will always be a weekDay
98
+ if isinstance(interval, str) and not interval.isnumeric():
99
+ interval_type = IntervalItem.Occurrence.WeekDay
83
100
 
84
- return [(interval_type, str(calculated_interval))]
101
+ interval_type_pairs.append((interval_type, str(interval)))
102
+ # otherwise the interval is hours
103
+ else:
104
+ interval_type = IntervalItem.Occurrence.Hours
105
+
106
+ interval_type_pairs.append((interval_type, str(interval)))
107
+
108
+ return interval_type_pairs
85
109
 
86
110
 
87
111
  class DailyInterval(object):
@@ -111,8 +135,45 @@ class DailyInterval(object):
111
135
  return self._interval
112
136
 
113
137
  @interval.setter
114
- def interval(self, interval):
115
- self._interval = interval
138
+ def interval(self, intervals):
139
+ VALID_INTERVALS = {0.25, 0.5, 1, 2, 4, 6, 8, 12}
140
+
141
+ for interval in intervals:
142
+ # if an hourly interval is a string, then it is a weekDay interval
143
+ if isinstance(interval, str) and not interval.isnumeric() and not hasattr(IntervalItem.Day, interval):
144
+ error = "Invalid weekDay interval {}".format(interval)
145
+ raise ValueError(error)
146
+
147
+ # if an hourly interval is a number, it is an hours or minutes interval
148
+ if isinstance(interval, (int, float)) and float(interval) not in VALID_INTERVALS:
149
+ error = "Invalid interval {} not in {}".format(interval, str(VALID_INTERVALS))
150
+ raise ValueError(error)
151
+
152
+ self._interval = intervals
153
+
154
+ def _interval_type_pairs(self):
155
+ interval_type_pairs = []
156
+ for interval in self.interval:
157
+ # We use fractional hours for the two minute-based intervals.
158
+ # Need to convert to minutes from hours here
159
+ if interval in {0.25, 0.5}:
160
+ calculated_interval = int(interval * 60)
161
+ interval_type = IntervalItem.Occurrence.Minutes
162
+
163
+ interval_type_pairs.append((interval_type, str(calculated_interval)))
164
+ else:
165
+ # if the interval is a non-numeric string, it will always be a weekDay
166
+ if isinstance(interval, str) and not interval.isnumeric():
167
+ interval_type = IntervalItem.Occurrence.WeekDay
168
+
169
+ interval_type_pairs.append((interval_type, str(interval)))
170
+ # otherwise the interval is hours
171
+ else:
172
+ interval_type = IntervalItem.Occurrence.Hours
173
+
174
+ interval_type_pairs.append((interval_type, str(interval)))
175
+
176
+ return interval_type_pairs
116
177
 
117
178
 
118
179
  class WeeklyInterval(object):
@@ -155,7 +216,12 @@ class WeeklyInterval(object):
155
216
  class MonthlyInterval(object):
156
217
  def __init__(self, start_time, interval_value):
157
218
  self.start_time = start_time
158
- self.interval = str(interval_value)
219
+
220
+ # interval should be a tuple, if it is not, assign as a tuple with single value
221
+ if isinstance(interval_value, tuple):
222
+ self.interval = interval_value
223
+ else:
224
+ self.interval = (interval_value,)
159
225
 
160
226
  def __repr__(self):
161
227
  return f"<{self.__class__.__name__} start={self.start_time} interval={self.interval}>"
@@ -179,24 +245,24 @@ class MonthlyInterval(object):
179
245
  return self._interval
180
246
 
181
247
  @interval.setter
182
- def interval(self, interval_value):
183
- error = "Invalid interval value for a monthly frequency: {}.".format(interval_value)
184
-
248
+ def interval(self, interval_values):
185
249
  # This is weird because the value could be a str or an int
186
250
  # The only valid str is 'LastDay' so we check that first. If that's not it
187
251
  # try to convert it to an int, if that fails because it's an incorrect string
188
252
  # like 'badstring' we catch and re-raise. Otherwise we convert to int and check
189
253
  # that it's in range 1-31
254
+ for interval_value in interval_values:
255
+ error = "Invalid interval value for a monthly frequency: {}.".format(interval_value)
190
256
 
191
- if interval_value != "LastDay":
192
- try:
193
- if not (1 <= int(interval_value) <= 31):
194
- raise ValueError(error)
195
- except ValueError:
196
- if interval_value != "LastDay":
197
- raise ValueError(error)
257
+ if interval_value != "LastDay":
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)
198
264
 
199
- self._interval = str(interval_value)
265
+ self._interval = interval_values
200
266
 
201
267
  def _interval_type_pairs(self):
202
268
  return [(IntervalItem.Occurrence.MonthDay, self.interval)]
@@ -163,9 +163,6 @@ class ProjectItem(object):
163
163
  attr,
164
164
  permissions,
165
165
  )
166
- fetch_call = getattr(self, attr)
167
- logging.getLogger().info({"type": attr, "value": fetch_call()})
168
- return fetch_call()
169
166
 
170
167
  @classmethod
171
168
  def from_response(cls, resp, ns) -> List["ProjectItem"]:
@@ -254,25 +254,43 @@ class ScheduleItem(object):
254
254
  interval.extend(interval_elem.attrib.items())
255
255
 
256
256
  if frequency == IntervalItem.Frequency.Daily:
257
- return DailyInterval(start_time)
257
+ converted_intervals = []
258
+
259
+ for i in interval:
260
+ # We use fractional hours for the two minute-based intervals.
261
+ # Need to convert to hours from minutes here
262
+ if i[0] == IntervalItem.Occurrence.Minutes:
263
+ converted_intervals.append(float(i[1]) / 60)
264
+ elif i[0] == IntervalItem.Occurrence.Hours:
265
+ converted_intervals.append(float(i[1]))
266
+ else:
267
+ converted_intervals.append(i[1])
268
+
269
+ return DailyInterval(start_time, *converted_intervals)
258
270
 
259
271
  if frequency == IntervalItem.Frequency.Hourly:
260
- interval_occurrence, interval_value = interval.pop()
272
+ converted_intervals = []
261
273
 
262
- # We use fractional hours for the two minute-based intervals.
263
- # Need to convert to hours from minutes here
264
- if interval_occurrence == IntervalItem.Occurrence.Minutes:
265
- interval_value = float(interval_value) / 60
274
+ for i in interval:
275
+ # We use fractional hours for the two minute-based intervals.
276
+ # Need to convert to hours from minutes here
277
+ if i[0] == IntervalItem.Occurrence.Minutes:
278
+ converted_intervals.append(float(i[1]) / 60)
279
+ elif i[0] == IntervalItem.Occurrence.Hours:
280
+ converted_intervals.append(i[1])
281
+ else:
282
+ converted_intervals.append(i[1])
266
283
 
267
- return HourlyInterval(start_time, end_time, interval_value)
284
+ return HourlyInterval(start_time, end_time, tuple(converted_intervals))
268
285
 
269
286
  if frequency == IntervalItem.Frequency.Weekly:
270
287
  interval_values = [i[1] for i in interval]
271
288
  return WeeklyInterval(start_time, *interval_values)
272
289
 
273
290
  if frequency == IntervalItem.Frequency.Monthly:
274
- interval_occurrence, interval_value = interval.pop()
275
- return MonthlyInterval(start_time, interval_value)
291
+ interval_values = [i[1] for i in interval]
292
+
293
+ return MonthlyInterval(start_time, tuple(interval_values))
276
294
 
277
295
  @staticmethod
278
296
  def _parse_element(schedule_xml, ns):
@@ -1,8 +1,11 @@
1
+ from datetime import datetime
2
+ from typing import List, Optional
3
+
1
4
  from defusedxml.ElementTree import fromstring
2
5
 
3
6
  from tableauserverclient.datetime_helpers import parse_datetime
4
- from .schedule_item import ScheduleItem
5
- from .target import Target
7
+ from tableauserverclient.models.schedule_item import ScheduleItem
8
+ from tableauserverclient.models.target import Target
6
9
 
7
10
 
8
11
  class TaskItem(object):
@@ -19,14 +22,14 @@ class TaskItem(object):
19
22
 
20
23
  def __init__(
21
24
  self,
22
- id_,
23
- task_type,
24
- priority,
25
- consecutive_failed_count=0,
26
- schedule_id=None,
27
- schedule_item=None,
28
- last_run_at=None,
29
- target=None,
25
+ id_: str,
26
+ task_type: str,
27
+ priority: int,
28
+ consecutive_failed_count: int = 0,
29
+ schedule_id: Optional[str] = None,
30
+ schedule_item: Optional[ScheduleItem] = None,
31
+ last_run_at: Optional[datetime] = None,
32
+ target: Optional[Target] = None,
30
33
  ):
31
34
  self.id = id_
32
35
  self.task_type = task_type
@@ -37,14 +40,14 @@ class TaskItem(object):
37
40
  self.last_run_at = last_run_at
38
41
  self.target = target
39
42
 
40
- def __repr__(self):
43
+ def __repr__(self) -> str:
41
44
  return (
42
45
  "<Task#{id} {task_type} pri({priority}) failed({consecutive_failed_count}) schedule_id({"
43
46
  "schedule_id}) target({target})>".format(**self.__dict__)
44
47
  )
45
48
 
46
49
  @classmethod
47
- def from_response(cls, xml, ns, task_type=Type.ExtractRefresh):
50
+ def from_response(cls, xml, ns, task_type=Type.ExtractRefresh) -> List["TaskItem"]:
48
51
  parsed_response = fromstring(xml)
49
52
  all_tasks_xml = parsed_response.findall(".//t:task/t:{}".format(task_type), namespaces=ns)
50
53
 
@@ -62,8 +65,7 @@ class TaskItem(object):
62
65
  last_run_at_element = element.find(".//t:lastRunAt", namespaces=ns)
63
66
 
64
67
  schedule_item_list = ScheduleItem.from_element(element, ns)
65
- if len(schedule_item_list) >= 1:
66
- schedule_item = schedule_item_list[0]
68
+ schedule_item = next(iter(schedule_item_list), None)
67
69
 
68
70
  # according to the Tableau Server REST API documentation,
69
71
  # there should be only one of workbook or datasource
@@ -87,14 +89,14 @@ class TaskItem(object):
87
89
  task_type,
88
90
  priority,
89
91
  consecutive_failed_count,
90
- schedule_item.id,
92
+ schedule_item.id if schedule_item is not None else None,
91
93
  schedule_item,
92
94
  last_run_at,
93
95
  target,
94
96
  )
95
97
 
96
98
  @staticmethod
97
- def _translate_task_type(task_type):
99
+ def _translate_task_type(task_type: str) -> str:
98
100
  if task_type in TaskItem._TASK_TYPE_MAPPING:
99
101
  return TaskItem._TASK_TYPE_MAPPING[task_type]
100
102
  else:
@@ -8,6 +8,8 @@ from contextlib import closing
8
8
  from pathlib import Path
9
9
  from typing import List, Mapping, Optional, Sequence, Tuple, TYPE_CHECKING, Union
10
10
 
11
+ from tableauserverclient.helpers.headers import fix_filename
12
+
11
13
  if TYPE_CHECKING:
12
14
  from tableauserverclient.server import Server
13
15
  from tableauserverclient.models import PermissionsRule
@@ -441,6 +443,7 @@ class Datasources(QuerysetEndpoint):
441
443
  filepath.write(chunk)
442
444
  return_path = filepath
443
445
  else:
446
+ params = fix_filename(params)
444
447
  filename = to_filename(os.path.basename(params["filename"]))
445
448
  download_path = make_download_path(filepath, filename)
446
449
  with open(download_path, "wb") as f:
@@ -1,8 +1,5 @@
1
- from threading import Thread
2
- from time import sleep
3
1
  from tableauserverclient import datetime_helpers as datetime
4
2
 
5
- import requests
6
3
  from packaging.version import Version
7
4
  from functools import wraps
8
5
  from xml.etree.ElementTree import ParseError
@@ -76,58 +73,21 @@ class Endpoint(object):
76
73
  # return explicitly for testing only
77
74
  return parameters
78
75
 
79
- def _blocking_request(self, method, url, parameters={}) -> Optional["Response"]:
80
- self.async_response = None
76
+ def _blocking_request(self, method, url, parameters={}) -> Optional[Union["Response", Exception]]:
81
77
  response = None
82
78
  logger.debug("[{}] Begin blocking request to {}".format(datetime.timestamp(), url))
83
79
  try:
84
80
  response = method(url, **parameters)
85
- self.async_response = response
86
81
  logger.debug("[{}] Call finished".format(datetime.timestamp()))
87
82
  except Exception as e:
88
83
  logger.debug("Error making request to server: {}".format(e))
89
- self.async_response = e
90
- finally:
91
- if response and not self.async_response:
92
- logger.debug("Request response not saved")
93
- return None
94
- logger.debug("[{}] Request complete".format(datetime.timestamp()))
95
- return self.async_response
84
+ raise e
85
+ return response
96
86
 
97
87
  def send_request_while_show_progress_threaded(
98
- self, method, url, parameters={}, request_timeout=0
99
- ) -> Optional["Response"]:
100
- try:
101
- request_thread = Thread(target=self._blocking_request, args=(method, url, parameters))
102
- request_thread.async_response = -1 # type:ignore # this is an invented attribute for thread comms
103
- request_thread.start()
104
- except Exception as e:
105
- logger.debug("Error starting server request on separate thread: {}".format(e))
106
- return None
107
- seconds = 0
108
- minutes = 0
109
- sleep(1)
110
- if self.async_response != -1:
111
- # a quick return for any immediate responses
112
- return self.async_response
113
- while self.async_response == -1 and (request_timeout == 0 or seconds < request_timeout):
114
- self.log_wait_time_then_sleep(minutes, seconds, url)
115
- seconds = seconds + DELAY_SLEEP_SECONDS
116
- if seconds >= 60:
117
- seconds = 0
118
- minutes = minutes + 1
119
- return self.async_response
120
-
121
- def log_wait_time_then_sleep(self, minutes, seconds, url):
122
- logger.debug("{} Waiting....".format(datetime.timestamp()))
123
- if seconds >= 60: # detailed log message ~every minute
124
- if minutes % 5 == 0:
125
- logger.info(
126
- "[{}] Waiting ({} minutes so far) for request to {}".format(datetime.timestamp(), minutes, url)
127
- )
128
- else:
129
- logger.debug("[{}] Waiting for request to {}".format(datetime.timestamp(), url))
130
- sleep(DELAY_SLEEP_SECONDS)
88
+ self, method, url, parameters={}, request_timeout=None
89
+ ) -> Optional[Union["Response", Exception]]:
90
+ return self._blocking_request(method, url, parameters)
131
91
 
132
92
  def _make_request(
133
93
  self,
@@ -151,7 +111,7 @@ class Endpoint(object):
151
111
  # a request can, for stuff like publishing, spin for ages waiting for a response.
152
112
  # we need some user-facing activity so they know it's not dead.
153
113
  request_timeout = self.parent_srv.http_options.get("timeout") or 0
154
- server_response: Optional["Response"] = self.send_request_while_show_progress_threaded(
114
+ server_response: Optional[Union["Response", Exception]] = self.send_request_while_show_progress_threaded(
155
115
  method, url, parameters, request_timeout
156
116
  )
157
117
  logger.debug("[{}] Async request returned: received {}".format(datetime.timestamp(), server_response))
@@ -163,6 +123,8 @@ class Endpoint(object):
163
123
  if server_response is None:
164
124
  logger.debug("[{}] Request failed".format(datetime.timestamp()))
165
125
  raise RuntimeError
126
+ if isinstance(server_response, Exception):
127
+ raise server_response
166
128
  self._check_status(server_response, url)
167
129
 
168
130
  loggable_response = self.log_response_safely(server_response)
@@ -7,6 +7,8 @@ from contextlib import closing
7
7
  from pathlib import Path
8
8
  from typing import Iterable, List, Optional, TYPE_CHECKING, Tuple, Union
9
9
 
10
+ from tableauserverclient.helpers.headers import fix_filename
11
+
10
12
  from .dqw_endpoint import _DataQualityWarningEndpoint
11
13
  from .endpoint import QuerysetEndpoint, api
12
14
  from .exceptions import InternalServerError, MissingRequiredFieldError
@@ -124,6 +126,7 @@ class Flows(QuerysetEndpoint):
124
126
  filepath.write(chunk)
125
127
  return_path = filepath
126
128
  else:
129
+ params = fix_filename(params)
127
130
  filename = to_filename(os.path.basename(params["filename"]))
128
131
  download_path = make_download_path(filepath, filename)
129
132
  with open(download_path, "wb") as f:
@@ -1,19 +1,23 @@
1
1
  import logging
2
+ from typing import List, Optional, Tuple, TYPE_CHECKING
2
3
 
3
- from .endpoint import Endpoint, api
4
- from .exceptions import MissingRequiredFieldError
4
+ from tableauserverclient.server.endpoint.endpoint import Endpoint, api
5
+ from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
5
6
  from tableauserverclient.models import TaskItem, PaginationItem
6
7
  from tableauserverclient.server import RequestFactory
7
8
 
8
9
  from tableauserverclient.helpers.logging import logger
9
10
 
11
+ if TYPE_CHECKING:
12
+ from tableauserverclient.server.request_options import RequestOptions
13
+
10
14
 
11
15
  class Tasks(Endpoint):
12
16
  @property
13
- def baseurl(self):
17
+ def baseurl(self) -> str:
14
18
  return "{0}/sites/{1}/tasks".format(self.parent_srv.baseurl, self.parent_srv.site_id)
15
19
 
16
- def __normalize_task_type(self, task_type):
20
+ def __normalize_task_type(self, task_type: str) -> str:
17
21
  """
18
22
  The word for extract refresh used in API URL is "extractRefreshes".
19
23
  It is different than the tag "extractRefresh" used in the request body.
@@ -24,11 +28,13 @@ class Tasks(Endpoint):
24
28
  return task_type
25
29
 
26
30
  @api(version="2.6")
27
- def get(self, req_options=None, task_type=TaskItem.Type.ExtractRefresh):
31
+ def get(
32
+ self, req_options: Optional["RequestOptions"] = None, task_type: str = TaskItem.Type.ExtractRefresh
33
+ ) -> Tuple[List[TaskItem], PaginationItem]:
28
34
  if task_type == TaskItem.Type.DataAcceleration:
29
35
  self.parent_srv.assert_at_least_version("3.8", "Data Acceleration Tasks")
30
36
 
31
- logger.info("Querying all {} tasks for the site".format(task_type))
37
+ logger.info("Querying all %s tasks for the site", task_type)
32
38
 
33
39
  url = "{0}/{1}".format(self.baseurl, self.__normalize_task_type(task_type))
34
40
  server_response = self.get_request(url, req_options)
@@ -38,11 +44,11 @@ class Tasks(Endpoint):
38
44
  return all_tasks, pagination_item
39
45
 
40
46
  @api(version="2.6")
41
- def get_by_id(self, task_id):
47
+ def get_by_id(self, task_id: str) -> TaskItem:
42
48
  if not task_id:
43
49
  error = "No Task ID provided"
44
50
  raise ValueError(error)
45
- logger.info("Querying a single task by id ({})".format(task_id))
51
+ logger.info("Querying a single task by id %s", task_id)
46
52
  url = "{}/{}/{}".format(
47
53
  self.baseurl,
48
54
  self.__normalize_task_type(TaskItem.Type.ExtractRefresh),
@@ -56,14 +62,14 @@ class Tasks(Endpoint):
56
62
  if not extract_item:
57
63
  error = "No extract refresh provided"
58
64
  raise ValueError(error)
59
- logger.info("Creating an extract refresh ({})".format(extract_item))
65
+ logger.info("Creating an extract refresh %s", extract_item)
60
66
  url = "{0}/{1}".format(self.baseurl, self.__normalize_task_type(TaskItem.Type.ExtractRefresh))
61
67
  create_req = RequestFactory.Task.create_extract_req(extract_item)
62
68
  server_response = self.post_request(url, create_req)
63
69
  return server_response.content
64
70
 
65
71
  @api(version="2.6")
66
- def run(self, task_item):
72
+ def run(self, task_item: TaskItem) -> bytes:
67
73
  if not task_item.id:
68
74
  error = "Task item missing ID."
69
75
  raise MissingRequiredFieldError(error)
@@ -79,7 +85,7 @@ class Tasks(Endpoint):
79
85
 
80
86
  # Delete 1 task by id
81
87
  @api(version="3.6")
82
- def delete(self, task_id, task_type=TaskItem.Type.ExtractRefresh):
88
+ def delete(self, task_id: str, task_type: str = TaskItem.Type.ExtractRefresh) -> None:
83
89
  if task_type == TaskItem.Type.DataAcceleration:
84
90
  self.parent_srv.assert_at_least_version("3.8", "Data Acceleration Tasks")
85
91
 
@@ -88,4 +94,4 @@ class Tasks(Endpoint):
88
94
  raise ValueError(error)
89
95
  url = "{0}/{1}/{2}".format(self.baseurl, self.__normalize_task_type(task_type), task_id)
90
96
  self.delete_request(url)
91
- logger.info("Deleted single task (ID: {0})".format(task_id))
97
+ logger.info("Deleted single task (ID: %s)", task_id)
@@ -6,6 +6,8 @@ import os
6
6
  from contextlib import closing
7
7
  from pathlib import Path
8
8
 
9
+ from tableauserverclient.helpers.headers import fix_filename
10
+
9
11
  from .endpoint import QuerysetEndpoint, api, parameter_added_in
10
12
  from .exceptions import InternalServerError, MissingRequiredFieldError
11
13
  from .permissions_endpoint import _PermissionsEndpoint
@@ -88,8 +90,8 @@ class Workbooks(QuerysetEndpoint):
88
90
  return WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)[0]
89
91
 
90
92
  @api(version="2.8")
91
- def refresh(self, workbook_id: str) -> JobItem:
92
- id_ = getattr(workbook_id, "id", workbook_id)
93
+ def refresh(self, workbook_item: Union[WorkbookItem, str]) -> JobItem:
94
+ id_ = getattr(workbook_item, "id", workbook_item)
93
95
  url = "{0}/{1}/refresh".format(self.baseurl, id_)
94
96
  empty_req = RequestFactory.Empty.empty_req()
95
97
  server_response = self.post_request(url, empty_req)
@@ -455,7 +457,7 @@ class Workbooks(QuerysetEndpoint):
455
457
  def download_revision(
456
458
  self,
457
459
  workbook_id: str,
458
- revision_number: str,
460
+ revision_number: Optional[str],
459
461
  filepath: Optional[PathOrFileW] = None,
460
462
  include_extract: bool = True,
461
463
  no_extract: Optional[bool] = None,
@@ -487,6 +489,7 @@ class Workbooks(QuerysetEndpoint):
487
489
  filepath.write(chunk)
488
490
  return_path = filepath
489
491
  else:
492
+ params = fix_filename(params)
490
493
  filename = to_filename(os.path.basename(params["filename"]))
491
494
  download_path = make_download_path(filepath, filename)
492
495
  with open(download_path, "wb") as f:
@@ -1032,6 +1032,16 @@ class TaskRequest(object):
1032
1032
  def create_extract_req(self, xml_request: ET.Element, extract_item: "TaskItem") -> bytes:
1033
1033
  extract_element = ET.SubElement(xml_request, "extractRefresh")
1034
1034
 
1035
+ # Main attributes
1036
+ extract_element.attrib["type"] = extract_item.task_type
1037
+
1038
+ if extract_item.target is not None:
1039
+ target_element = ET.SubElement(extract_element, extract_item.target.type)
1040
+ target_element.attrib["id"] = extract_item.target.id
1041
+
1042
+ if extract_item.schedule_item is None:
1043
+ return ET.tostring(xml_request)
1044
+
1035
1045
  # Schedule attributes
1036
1046
  schedule_element = ET.SubElement(xml_request, "schedule")
1037
1047
 
@@ -1043,17 +1053,11 @@ class TaskRequest(object):
1043
1053
  frequency_element.attrib["end"] = str(interval_item.end_time)
1044
1054
  if hasattr(interval_item, "interval") and interval_item.interval:
1045
1055
  intervals_element = ET.SubElement(frequency_element, "intervals")
1046
- for interval in interval_item._interval_type_pairs():
1056
+ for interval in interval_item._interval_type_pairs(): # type: ignore
1047
1057
  expression, value = interval
1048
1058
  single_interval_element = ET.SubElement(intervals_element, "interval")
1049
1059
  single_interval_element.attrib[expression] = value
1050
1060
 
1051
- # Main attributes
1052
- extract_element.attrib["type"] = extract_item.task_type
1053
-
1054
- target_element = ET.SubElement(extract_element, extract_item.target.type)
1055
- target_element.attrib["id"] = extract_item.target.id
1056
-
1057
1061
  return ET.tostring(xml_request)
1058
1062
 
1059
1063
 
@@ -37,35 +37,75 @@ class RequestOptions(RequestOptionsBase):
37
37
 
38
38
  class Field:
39
39
  Args = "args"
40
+ AuthenticationType = "authenticationType"
41
+ Caption = "caption"
42
+ Channel = "channel"
40
43
  CompletedAt = "completedAt"
44
+ ConnectedWorkbookType = "connectedWorkbookType"
45
+ ConnectionTo = "connectionTo"
46
+ ConnectionType = "connectionType"
41
47
  ContentUrl = "contentUrl"
42
48
  CreatedAt = "createdAt"
49
+ DatabaseName = "databaseName"
50
+ DatabaseUserName = "databaseUserName"
51
+ Description = "description"
52
+ DisplayTabs = "displayTabs"
43
53
  DomainName = "domainName"
44
54
  DomainNickname = "domainNickname"
55
+ FavoritesTotal = "favoritesTotal"
56
+ Fields = "fields"
57
+ FlowId = "flowId"
58
+ FriendlyName = "friendlyName"
59
+ HasAlert = "hasAlert"
60
+ HasAlerts = "hasAlerts"
61
+ HasEmbeddedPassword = "hasEmbeddedPassword"
62
+ HasExtracts = "hasExtracts"
45
63
  HitsTotal = "hitsTotal"
64
+ Id = "id"
65
+ IsCertified = "isCertified"
66
+ IsConnectable = "isConnectable"
67
+ IsDefaultPort = "isDefaultPort"
68
+ IsHierarchical = "isHierarchical"
46
69
  IsLocal = "isLocal"
70
+ IsPublished = "isPublished"
47
71
  JobType = "jobType"
48
72
  LastLogin = "lastLogin"
73
+ Luid = "luid"
49
74
  MinimumSiteRole = "minimumSiteRole"
50
75
  Name = "name"
51
76
  Notes = "notes"
77
+ NotificationType = "notificationType"
52
78
  OwnerDomain = "ownerDomain"
53
79
  OwnerEmail = "ownerEmail"
54
80
  OwnerName = "ownerName"
55
81
  ParentProjectId = "parentProjectId"
82
+ Priority = "priority"
56
83
  Progress = "progress"
84
+ ProjectId = "projectId"
57
85
  ProjectName = "projectName"
58
86
  PublishSamples = "publishSamples"
87
+ ServerName = "serverName"
88
+ ServerPort = "serverPort"
89
+ SheetCount = "sheetCount"
90
+ SheetNumber = "sheetNumber"
91
+ SheetType = "sheetType"
59
92
  SiteRole = "siteRole"
93
+ Size = "size"
60
94
  StartedAt = "startedAt"
61
95
  Status = "status"
96
+ SubscriptionsTotal = "subscriptionsTotal"
62
97
  Subtitle = "subtitle"
98
+ TableName = "tableName"
63
99
  Tags = "tags"
64
100
  Title = "title"
65
101
  TopLevelProject = "topLevelProject"
66
102
  Type = "type"
67
103
  UpdatedAt = "updatedAt"
68
104
  UserCount = "userCount"
105
+ UserId = "userId"
106
+ ViewUrlName = "viewUrlName"
107
+ WorkbookDescription = "workbookDescription"
108
+ WorkbookName = "workbookName"
69
109
 
70
110
  class Direction:
71
111
  Desc = "desc"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tableauserverclient
3
- Version: 0.28
3
+ Version: 0.30
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)
@@ -40,7 +40,7 @@ License-File: LICENSE.versioneer
40
40
  Requires-Dist: defusedxml >=0.7.1
41
41
  Requires-Dist: packaging >=23.1
42
42
  Requires-Dist: requests >=2.31
43
- Requires-Dist: urllib3 ==2.0.6
43
+ Requires-Dist: urllib3 ==2.0.7
44
44
  Provides-Extra: test
45
45
  Requires-Dist: argparse ; extra == 'test'
46
46
  Requires-Dist: black ==23.7 ; extra == 'test'
@@ -1,14 +1,15 @@
1
1
  tableauserverclient/__init__.py,sha256=hDjLTdwXPmQj4BQBRdUcnP7XdhT7apJ_c7JAPyyYZ7Y,1096
2
- tableauserverclient/_version.py,sha256=77BR3Uhg7xOImjTULb4qcvv7VjeMtNO9xpnQ-eW_gLU,496
3
- tableauserverclient/config.py,sha256=u9YnTDv0XnFR0sUY6AdeSuP5Zi2WKmcgJGGdehuaoO0,429
2
+ tableauserverclient/_version.py,sha256=1DZOBnxMpAUaNuob4jPBOvm74gFOHdcIzHXiKW4g43M,496
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
6
6
  tableauserverclient/filesys_helpers.py,sha256=hosTm9fpc9B9_VCDcAuyHA0A-f-MWs9_2utyRweuZ5I,1667
7
7
  tableauserverclient/namespace.py,sha256=i0o0T0xO0vd1RX98o54xmIrqCkeVfhtQOGDMGRx_F_o,971
8
8
  tableauserverclient/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  tableauserverclient/helpers/__init__.py,sha256=llpqF9zV5dsP5hQt8dyop33Op5OzGmxW0BZibaSlCcM,23
10
+ tableauserverclient/helpers/headers.py,sha256=Pg-ZWSgV2Yp813BV1Tzo8F2Hn4P01zscla2RRM4yqNk,411
10
11
  tableauserverclient/helpers/logging.py,sha256=5q_oR3wERHVXFXY6XDnLYML5-HdAPJTmqhH9IZg4VfY,113
11
- tableauserverclient/helpers/strings.py,sha256=dnfm_2s75WvXnkcU8JFGRlLbSZdarRPyHpRYOi19APs,1682
12
+ tableauserverclient/helpers/strings.py,sha256=HRTP31HIdD3gFxDDHuBsmOl_8cLdh8fa3xLALgYsUAQ,1563
12
13
  tableauserverclient/models/__init__.py,sha256=NfVfRDeU8b6zTQ7wzkorhvuGIPabym8lXvNiyEX_AVg,1620
13
14
  tableauserverclient/models/column_item.py,sha256=gV2r7xOVmcn9UnMXFOe9_ORZ85oha1KtIH-crKUhgN4,1972
14
15
  tableauserverclient/models/connection_credentials.py,sha256=8Wa8B_-cB3INFUup-Pyd5znsUD5opgYYH5TfipCzKTU,1559
@@ -25,16 +26,16 @@ tableauserverclient/models/fileupload_item.py,sha256=tx3LfgRxVqeHGpCN6kTDPkdBY5i
25
26
  tableauserverclient/models/flow_item.py,sha256=rPIVDe8_tHied8TDRiGpg2gyTyQWlNiGb3vGhqF9YJw,7320
26
27
  tableauserverclient/models/flow_run_item.py,sha256=pwJNz0m7l--Sb6z0o83ebWfAmTVFYSr-0apMxHWt0Vs,3139
27
28
  tableauserverclient/models/group_item.py,sha256=1KP_KmIG1veJJGyte82aFvMAiU_tvzYtaP2vLEReLoo,3621
28
- tableauserverclient/models/interval_item.py,sha256=h0mAFnR3rOj9nNOT237sPHQrdkN3NyonnGX10Chbx9k,5747
29
+ tableauserverclient/models/interval_item.py,sha256=uSYQskXWlPFoQHC2wz58j6G93qhVKepMTYeWW9FbGaY,9053
29
30
  tableauserverclient/models/job_item.py,sha256=PMt7G2qu8WAxe-b3f_0BtQ9JcCSvk-g5L5EcOUpYFCY,8619
30
31
  tableauserverclient/models/metric_item.py,sha256=Zrzi_Un43p2jzE_4OhetxhjfeSUYzotMfm8Hjd5WOhI,5397
31
32
  tableauserverclient/models/pagination_item.py,sha256=Hdr2EIKWkXfrjWOr-Ao_vzQTWB2akUfxxtaupFKLrlQ,1432
32
33
  tableauserverclient/models/permissions_item.py,sha256=2WlJOc1Vxkxl-WnAkFaacWsGxUXU9_d85AetSKrKwP0,3731
33
- tableauserverclient/models/project_item.py,sha256=Mf5maEdKJqk5ojhT_DNmLqL50Ib6u1OKYEDq0oJpeGs,7056
34
+ tableauserverclient/models/project_item.py,sha256=DNxAhkMkRt9MLcMie4w8nuTG4sApIkt618onBEdoM_c,6915
34
35
  tableauserverclient/models/property_decorators.py,sha256=eXaYxzXYuHdftlcnqSQOzHYM7OAilETWK8Td2Dtmmuc,4726
35
36
  tableauserverclient/models/reference_item.py,sha256=HddG1loGNJU7gB7hBccCjnhagnSEbk4_wY2kuPbNhRQ,534
36
37
  tableauserverclient/models/revision_item.py,sha256=fLkcScAgBBiVMCyDPk4aboyZNxrykIavzJP2vkn6Rq0,2787
37
- tableauserverclient/models/schedule_item.py,sha256=AmodNmh9iKdlGzyEm0pRjdy8-yFSXsIe7xf0Y6oVFVc,10569
38
+ tableauserverclient/models/schedule_item.py,sha256=wSkOz9OUJol_aznOl66ZMDz9Q0Y-LOOc3me8X-ysV28,11324
38
39
  tableauserverclient/models/server_info_item.py,sha256=pR_fqqPuiBwpLn0wr4rQ4vxK7gxGJVTtaswzwjslheE,1880
39
40
  tableauserverclient/models/site_item.py,sha256=xvXpsR0eEz2ypS-94KJTMS-gusHV1bLomKIhWX7GXrU,42256
40
41
  tableauserverclient/models/subscription_item.py,sha256=XkQiAi_gWPBnxtTaB8TgrFa-IDVDtai9H00Ilay0T64,4744
@@ -43,7 +44,7 @@ tableauserverclient/models/tableau_auth.py,sha256=TdkOtRnNRi4p0w_WhBsWqUunNVG9nx
43
44
  tableauserverclient/models/tableau_types.py,sha256=1iq1034K1W9ugLAxON_t8n2UxSouiWV_ZEwoHiNM94c,918
44
45
  tableauserverclient/models/tag_item.py,sha256=mVfJGZG8PspPM8hBxX38heEQ_Rnp5sSwfwWdLNA-UV0,619
45
46
  tableauserverclient/models/target.py,sha256=dVc1SHwo9ZASR8250MhAPODXX_G5UqA4BnK8hykRE6E,263
46
- tableauserverclient/models/task_item.py,sha256=0DSOZ3aQ6nppD-zVeeK-GKdAxLE0-d-sSazXs7ybH6Q,3466
47
+ tableauserverclient/models/task_item.py,sha256=o0xm588Mc6WGGbYaY0eAxdYog5Z_lfWp1vripK7rKPI,3738
47
48
  tableauserverclient/models/user_item.py,sha256=vaQD3nvU7bJVD6t0nkWQFse8UDz8HTp8wh_6WmCjKtM,15776
48
49
  tableauserverclient/models/view_item.py,sha256=7OLTLpLXDiYAWi7Tc6vSCkh2Krl7-cvKUwA7iMWZspQ,6777
49
50
  tableauserverclient/models/webhook_item.py,sha256=LLf2HQ2Y50Rw-c4dSYyv6MXLbXdij9Iyy261gu9zpv4,2650
@@ -53,8 +54,8 @@ tableauserverclient/server/exceptions.py,sha256=l-O4lcoEeXJC7fLWUanIcZM_Mf8m54uK
53
54
  tableauserverclient/server/filter.py,sha256=WZ_dIBuADRZf8VTYcRgn1rulSGUOJq8_uUltq05KkRI,1119
54
55
  tableauserverclient/server/pager.py,sha256=rd6rKkoTxP0u3RiOZ3HY9oWKyOcf8aowbHPcs-Ul4D0,2725
55
56
  tableauserverclient/server/query.py,sha256=qMweo-N5P_V9z2Or4u_QrStaBbGL6HOg4Y4EDg8uh44,5735
56
- tableauserverclient/server/request_factory.py,sha256=u8h2EtGKBAN5ELR9Cg_v3bSKvRsMmy4IXVkYp4OZqzQ,59190
57
- tableauserverclient/server/request_options.py,sha256=xilsnYTsNObCZfwB5ydfzYq7XBkhpvIMWHM67yWQSKY,7105
57
+ tableauserverclient/server/request_factory.py,sha256=br5JwdVFbo9gU2kvg1DUzMnYN31i4UcdLWXwpMheMoY,59350
58
+ tableauserverclient/server/request_options.py,sha256=JiibNhk9QHI1houbtb4NYD4-y9GLxC98iArZJ1E8GUQ,8553
58
59
  tableauserverclient/server/server.py,sha256=LhbQOk-lvIBAOF5tTAghYNPFsKJN4Z95SJbuHLAxdKo,8401
59
60
  tableauserverclient/server/sort.py,sha256=ikndqXW2r2FeacJzybC2TVcJGn4ktviWgXwPyK-AX-0,208
60
61
  tableauserverclient/server/endpoint/__init__.py,sha256=9KGDtgBWlWsWbBQvQV2cbIU-VmHmMqcgtSM7PonpIzg,1141
@@ -63,15 +64,15 @@ tableauserverclient/server/endpoint/custom_views_endpoint.py,sha256=SShsI8TnDe3t
63
64
  tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py,sha256=nfC2gXoCke-YXGTiMWGDRBK_lDlLa9Y6hts7w3Zs8cI,1170
64
65
  tableauserverclient/server/endpoint/data_alert_endpoint.py,sha256=c6-ICGRVDRQN7ClAgCeJsk6JK4HyBvtQGQVLOzNc4eo,5358
65
66
  tableauserverclient/server/endpoint/databases_endpoint.py,sha256=0V6cHwF5E1K1oQ9JKT-rb7t1EFPNwlSKMC2HyqEqYPw,5366
66
- tableauserverclient/server/endpoint/datasources_endpoint.py,sha256=f51jAj3yr6TssOLygEBmJIZo8y__fRNKbjxnV_yBTDo,20299
67
+ tableauserverclient/server/endpoint/datasources_endpoint.py,sha256=XjoPiejyCGIze6gMO_ALZGWS44vFJ8LxroYr7Xq1OXE,20407
67
68
  tableauserverclient/server/endpoint/default_permissions_endpoint.py,sha256=HQSYPIJwVCimuWo4YGYvsc41_-rweLHmJ-tQrWdH-Y8,4386
68
69
  tableauserverclient/server/endpoint/dqw_endpoint.py,sha256=TvzjonvIiGtp4EisoBKms1lHOkVTjagQACw6unyY9_8,2418
69
- tableauserverclient/server/endpoint/endpoint.py,sha256=IZtHZia7aSr2sc1iV8bdtg39kX-IBDsXLoAxKa3y5v8,14425
70
+ tableauserverclient/server/endpoint/endpoint.py,sha256=8-XteqZmfuHDyqR_mGSXKZm9esWbeybaZByHEZoBEac,12743
70
71
  tableauserverclient/server/endpoint/exceptions.py,sha256=fh4fFYasqiKd3EPTxriVsmLkRL24wz9UvX94BMRfyLY,2534
71
72
  tableauserverclient/server/endpoint/favorites_endpoint.py,sha256=D0MCco9kxaeCShfWOnVt3UpttJRgczJmJKXb3w0jpw4,6701
72
73
  tableauserverclient/server/endpoint/fileuploads_endpoint.py,sha256=fh2xH6a8mMcBI14QzodWPBMabK1_9SMDbzZbawz-ZJc,2553
73
74
  tableauserverclient/server/endpoint/flow_runs_endpoint.py,sha256=1vwkOrb574natWl-njFOij8xaV32e9RU3-rFbu4bML0,3338
74
- tableauserverclient/server/endpoint/flows_endpoint.py,sha256=97S15UatgSzdw22Q5ZfhtlVVTvahp7fAtrXt7L28OWg,12898
75
+ tableauserverclient/server/endpoint/flows_endpoint.py,sha256=DIHQUb5LHSerYXsEmaRoiNcdeQ2tU_RaBdr23NnLG2Y,13006
75
76
  tableauserverclient/server/endpoint/groups_endpoint.py,sha256=_aSTVy7m3NXSEqgC0cPfJt6KOaPoWprxzz0MD8CsiiY,6640
76
77
  tableauserverclient/server/endpoint/jobs_endpoint.py,sha256=P5Hdv1hsNVxd5KdrcNCfR9ZtJ7v3UgrfATCBItVdAxY,3239
77
78
  tableauserverclient/server/endpoint/metadata_endpoint.py,sha256=AKnmRNj9U85p__igiuY_vhhipsxNXpQ_9zzNUtTuU9o,5203
@@ -84,14 +85,14 @@ tableauserverclient/server/endpoint/server_info_endpoint.py,sha256=4TtCc7oIp6iEu
84
85
  tableauserverclient/server/endpoint/sites_endpoint.py,sha256=lMeVXxm1k32HZK84u7aTys9hUQekb4PE1hlhKFxaZ8c,6694
85
86
  tableauserverclient/server/endpoint/subscriptions_endpoint.py,sha256=mMo9M6DF6tAZIab6UY8f_duETezELl9VXEYs5ocAcGY,3280
86
87
  tableauserverclient/server/endpoint/tables_endpoint.py,sha256=OJQIarL-IC9mjeu1jfNh9B3UEkB41O51Q53TQNeSJ78,5376
87
- tableauserverclient/server/endpoint/tasks_endpoint.py,sha256=jHz-d3q-hoxmv9Xg2qPAOzAz9mv0ahHtPSW8IXon0sw,3707
88
+ tableauserverclient/server/endpoint/tasks_endpoint.py,sha256=HjtSGXBGS8Eriaf5bSWySiqLmKQR_rU4Ao3wGz7Xpd8,4060
88
89
  tableauserverclient/server/endpoint/users_endpoint.py,sha256=QRoApMyP53nF-AqvC7oLHNcT4VJE8s5ykjUnI5ojmPY,7274
89
90
  tableauserverclient/server/endpoint/views_endpoint.py,sha256=GIYPftL5B0YUOb_SisFtfLP0LPIAbbUv24XCi15UH_M,7118
90
91
  tableauserverclient/server/endpoint/webhooks_endpoint.py,sha256=-HsbAuKECNcx5EZkqp097PfGHLCNwPRnxbrdzreTpnM,2835
91
- tableauserverclient/server/endpoint/workbooks_endpoint.py,sha256=65Zv-b0yd4yuh1JAoqbhNlEJM6k2LD6nh1nPQGTdKRc,21734
92
- tableauserverclient-0.28.dist-info/LICENSE,sha256=MMkY7MguOb4L-WCmmqrVcwg2iwSGaz-RfAMFvXRXwsQ,1074
93
- tableauserverclient-0.28.dist-info/LICENSE.versioneer,sha256=OYaGozOXk7bUNSm1z577iDYpti8a40XIqqR4lyQ47D8,334
94
- tableauserverclient-0.28.dist-info/METADATA,sha256=0oM0-poO__vHSlu02Lfa93Lv8lRqYsNn5RI5B6UIRvI,4073
95
- tableauserverclient-0.28.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
96
- tableauserverclient-0.28.dist-info/top_level.txt,sha256=kBnL39G2RlGqxJSsShDBWG4WZ3NFcWJkv1EGiOfZh4Q,20
97
- tableauserverclient-0.28.dist-info/RECORD,,
92
+ tableauserverclient/server/endpoint/workbooks_endpoint.py,sha256=_Tfaioe4QBA7VyswtxRu8uL-CzIK0wtslkwj41Wdx_4,21879
93
+ tableauserverclient-0.30.dist-info/LICENSE,sha256=MMkY7MguOb4L-WCmmqrVcwg2iwSGaz-RfAMFvXRXwsQ,1074
94
+ tableauserverclient-0.30.dist-info/LICENSE.versioneer,sha256=OYaGozOXk7bUNSm1z577iDYpti8a40XIqqR4lyQ47D8,334
95
+ tableauserverclient-0.30.dist-info/METADATA,sha256=K7aLh-r1p6dPxb6juSz5gB-fTepGAJOaVzeDABBuk9M,4073
96
+ tableauserverclient-0.30.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
97
+ tableauserverclient-0.30.dist-info/top_level.txt,sha256=kBnL39G2RlGqxJSsShDBWG4WZ3NFcWJkv1EGiOfZh4Q,20
98
+ tableauserverclient-0.30.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.2)
2
+ Generator: bdist_wheel (0.42.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5