tableauserverclient 0.32__py3-none-any.whl → 0.34__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tableauserverclient/__init__.py +34 -18
- tableauserverclient/_version.py +3 -3
- tableauserverclient/config.py +20 -6
- tableauserverclient/models/__init__.py +12 -0
- tableauserverclient/models/column_item.py +1 -1
- tableauserverclient/models/connection_credentials.py +1 -1
- tableauserverclient/models/connection_item.py +10 -8
- tableauserverclient/models/custom_view_item.py +29 -6
- tableauserverclient/models/data_acceleration_report_item.py +2 -2
- tableauserverclient/models/data_alert_item.py +5 -5
- tableauserverclient/models/data_freshness_policy_item.py +6 -6
- tableauserverclient/models/database_item.py +8 -2
- tableauserverclient/models/datasource_item.py +10 -10
- tableauserverclient/models/dqw_item.py +1 -1
- tableauserverclient/models/favorites_item.py +5 -6
- tableauserverclient/models/fileupload_item.py +1 -1
- tableauserverclient/models/flow_item.py +12 -12
- tableauserverclient/models/flow_run_item.py +3 -3
- tableauserverclient/models/group_item.py +4 -4
- tableauserverclient/models/groupset_item.py +53 -0
- tableauserverclient/models/interval_item.py +36 -23
- tableauserverclient/models/job_item.py +26 -10
- tableauserverclient/models/linked_tasks_item.py +102 -0
- tableauserverclient/models/metric_item.py +5 -5
- tableauserverclient/models/pagination_item.py +1 -1
- tableauserverclient/models/permissions_item.py +19 -14
- tableauserverclient/models/project_item.py +35 -19
- tableauserverclient/models/property_decorators.py +12 -11
- tableauserverclient/models/reference_item.py +2 -2
- tableauserverclient/models/revision_item.py +3 -3
- tableauserverclient/models/schedule_item.py +2 -2
- tableauserverclient/models/server_info_item.py +26 -6
- tableauserverclient/models/site_item.py +69 -3
- tableauserverclient/models/subscription_item.py +3 -3
- tableauserverclient/models/table_item.py +1 -1
- tableauserverclient/models/tableau_auth.py +115 -5
- tableauserverclient/models/tableau_types.py +11 -9
- tableauserverclient/models/tag_item.py +3 -4
- tableauserverclient/models/task_item.py +4 -4
- tableauserverclient/models/user_item.py +47 -17
- tableauserverclient/models/view_item.py +11 -10
- tableauserverclient/models/virtual_connection_item.py +78 -0
- tableauserverclient/models/webhook_item.py +6 -6
- tableauserverclient/models/workbook_item.py +90 -12
- tableauserverclient/namespace.py +1 -1
- tableauserverclient/server/__init__.py +2 -1
- tableauserverclient/server/endpoint/__init__.py +8 -0
- tableauserverclient/server/endpoint/auth_endpoint.py +68 -11
- tableauserverclient/server/endpoint/custom_views_endpoint.py +124 -19
- tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py +2 -2
- tableauserverclient/server/endpoint/data_alert_endpoint.py +14 -14
- tableauserverclient/server/endpoint/databases_endpoint.py +32 -17
- tableauserverclient/server/endpoint/datasources_endpoint.py +150 -59
- tableauserverclient/server/endpoint/default_permissions_endpoint.py +19 -18
- tableauserverclient/server/endpoint/dqw_endpoint.py +9 -9
- tableauserverclient/server/endpoint/endpoint.py +47 -31
- tableauserverclient/server/endpoint/exceptions.py +23 -7
- tableauserverclient/server/endpoint/favorites_endpoint.py +31 -31
- tableauserverclient/server/endpoint/fileuploads_endpoint.py +11 -13
- tableauserverclient/server/endpoint/flow_runs_endpoint.py +59 -17
- tableauserverclient/server/endpoint/flow_task_endpoint.py +2 -2
- tableauserverclient/server/endpoint/flows_endpoint.py +73 -35
- tableauserverclient/server/endpoint/groups_endpoint.py +96 -27
- tableauserverclient/server/endpoint/groupsets_endpoint.py +127 -0
- tableauserverclient/server/endpoint/jobs_endpoint.py +79 -12
- tableauserverclient/server/endpoint/linked_tasks_endpoint.py +45 -0
- tableauserverclient/server/endpoint/metadata_endpoint.py +2 -2
- tableauserverclient/server/endpoint/metrics_endpoint.py +10 -10
- tableauserverclient/server/endpoint/permissions_endpoint.py +13 -15
- tableauserverclient/server/endpoint/projects_endpoint.py +124 -30
- tableauserverclient/server/endpoint/resource_tagger.py +139 -6
- tableauserverclient/server/endpoint/schedules_endpoint.py +17 -18
- tableauserverclient/server/endpoint/server_info_endpoint.py +40 -5
- tableauserverclient/server/endpoint/sites_endpoint.py +282 -17
- tableauserverclient/server/endpoint/subscriptions_endpoint.py +10 -10
- tableauserverclient/server/endpoint/tables_endpoint.py +33 -19
- tableauserverclient/server/endpoint/tasks_endpoint.py +8 -8
- tableauserverclient/server/endpoint/users_endpoint.py +405 -19
- tableauserverclient/server/endpoint/views_endpoint.py +111 -25
- tableauserverclient/server/endpoint/virtual_connections_endpoint.py +174 -0
- tableauserverclient/server/endpoint/webhooks_endpoint.py +11 -11
- tableauserverclient/server/endpoint/workbooks_endpoint.py +735 -68
- tableauserverclient/server/filter.py +2 -2
- tableauserverclient/server/pager.py +8 -10
- tableauserverclient/server/query.py +70 -20
- tableauserverclient/server/request_factory.py +213 -41
- tableauserverclient/server/request_options.py +125 -145
- tableauserverclient/server/server.py +73 -9
- tableauserverclient/server/sort.py +2 -2
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/METADATA +17 -17
- tableauserverclient-0.34.dist-info/RECORD +106 -0
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/WHEEL +1 -1
- tableauserverclient-0.32.dist-info/RECORD +0 -100
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/LICENSE +0 -0
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/LICENSE.versioneer +0 -0
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/top_level.txt +0 -0
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import xml.etree.ElementTree as ET
|
|
2
|
-
from typing import Set
|
|
3
2
|
|
|
4
3
|
from defusedxml.ElementTree import fromstring
|
|
5
4
|
|
|
6
5
|
|
|
7
|
-
class TagItem
|
|
6
|
+
class TagItem:
|
|
8
7
|
@classmethod
|
|
9
|
-
def from_response(cls, resp: bytes, ns) ->
|
|
8
|
+
def from_response(cls, resp: bytes, ns) -> set[str]:
|
|
10
9
|
return cls.from_xml_element(fromstring(resp), ns)
|
|
11
10
|
|
|
12
11
|
@classmethod
|
|
13
|
-
def from_xml_element(cls, parsed_response: ET.Element, ns) ->
|
|
12
|
+
def from_xml_element(cls, parsed_response: ET.Element, ns) -> set[str]:
|
|
14
13
|
all_tags = set()
|
|
15
14
|
tag_elem = parsed_response.findall(".//t:tag", namespaces=ns)
|
|
16
15
|
for tag_xml in tag_elem:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Optional
|
|
3
3
|
|
|
4
4
|
from defusedxml.ElementTree import fromstring
|
|
5
5
|
|
|
@@ -8,7 +8,7 @@ from tableauserverclient.models.schedule_item import ScheduleItem
|
|
|
8
8
|
from tableauserverclient.models.target import Target
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class TaskItem
|
|
11
|
+
class TaskItem:
|
|
12
12
|
class Type:
|
|
13
13
|
ExtractRefresh = "extractRefresh"
|
|
14
14
|
DataAcceleration = "dataAcceleration"
|
|
@@ -48,9 +48,9 @@ class TaskItem(object):
|
|
|
48
48
|
)
|
|
49
49
|
|
|
50
50
|
@classmethod
|
|
51
|
-
def from_response(cls, xml, ns, task_type=Type.ExtractRefresh) ->
|
|
51
|
+
def from_response(cls, xml, ns, task_type=Type.ExtractRefresh) -> list["TaskItem"]:
|
|
52
52
|
parsed_response = fromstring(xml)
|
|
53
|
-
all_tasks_xml = parsed_response.findall(".//t:task/t:{}"
|
|
53
|
+
all_tasks_xml = parsed_response.findall(f".//t:task/t:{task_type}", namespaces=ns)
|
|
54
54
|
|
|
55
55
|
all_tasks = (TaskItem._parse_element(x, ns) for x in all_tasks_xml)
|
|
56
56
|
|
|
@@ -2,7 +2,7 @@ import io
|
|
|
2
2
|
import xml.etree.ElementTree as ET
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from enum import IntEnum
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Optional, TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
from defusedxml.ElementTree import fromstring
|
|
8
8
|
|
|
@@ -18,10 +18,35 @@ if TYPE_CHECKING:
|
|
|
18
18
|
from tableauserverclient.server import Pager
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
class UserItem
|
|
21
|
+
class UserItem:
|
|
22
|
+
"""
|
|
23
|
+
The UserItem class contains the members or attributes for the view
|
|
24
|
+
resources on Tableau Server. The UserItem class defines the information you
|
|
25
|
+
can request or query from Tableau Server. The class attributes correspond
|
|
26
|
+
to the attributes of a server request or response payload.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
name: str
|
|
32
|
+
The name of the user.
|
|
33
|
+
|
|
34
|
+
site_role: str
|
|
35
|
+
The role of the user on the site.
|
|
36
|
+
|
|
37
|
+
auth_setting: str
|
|
38
|
+
Required attribute for Tableau Cloud. How the user autenticates to the
|
|
39
|
+
server.
|
|
40
|
+
"""
|
|
41
|
+
|
|
22
42
|
tag_name: str = "user"
|
|
23
43
|
|
|
24
44
|
class Roles:
|
|
45
|
+
"""
|
|
46
|
+
The Roles class contains the possible roles for a user on Tableau
|
|
47
|
+
Server.
|
|
48
|
+
"""
|
|
49
|
+
|
|
25
50
|
Interactor = "Interactor"
|
|
26
51
|
Publisher = "Publisher"
|
|
27
52
|
ServerAdministrator = "ServerAdministrator"
|
|
@@ -43,6 +68,11 @@ class UserItem(object):
|
|
|
43
68
|
SupportUser = "SupportUser"
|
|
44
69
|
|
|
45
70
|
class Auth:
|
|
71
|
+
"""
|
|
72
|
+
The Auth class contains the possible authentication settings for a user
|
|
73
|
+
on Tableau Cloud.
|
|
74
|
+
"""
|
|
75
|
+
|
|
46
76
|
OpenID = "OpenID"
|
|
47
77
|
SAML = "SAML"
|
|
48
78
|
TableauIDWithMFA = "TableauIDWithMFA"
|
|
@@ -57,7 +87,7 @@ class UserItem(object):
|
|
|
57
87
|
self._id: Optional[str] = None
|
|
58
88
|
self._last_login: Optional[datetime] = None
|
|
59
89
|
self._workbooks = None
|
|
60
|
-
self._favorites: Optional[
|
|
90
|
+
self._favorites: Optional[dict[str, list]] = None
|
|
61
91
|
self._groups = None
|
|
62
92
|
self.email: Optional[str] = None
|
|
63
93
|
self.fullname: Optional[str] = None
|
|
@@ -69,7 +99,7 @@ class UserItem(object):
|
|
|
69
99
|
|
|
70
100
|
def __str__(self) -> str:
|
|
71
101
|
str_site_role = self.site_role or "None"
|
|
72
|
-
return "<User {} name={} role={}>"
|
|
102
|
+
return f"<User {self.id} name={self.name} role={str_site_role}>"
|
|
73
103
|
|
|
74
104
|
def __repr__(self):
|
|
75
105
|
return self.__str__() + " { " + ", ".join(" % s: % s" % item for item in vars(self).items()) + "}"
|
|
@@ -141,7 +171,7 @@ class UserItem(object):
|
|
|
141
171
|
return self._workbooks()
|
|
142
172
|
|
|
143
173
|
@property
|
|
144
|
-
def favorites(self) ->
|
|
174
|
+
def favorites(self) -> dict[str, list]:
|
|
145
175
|
if self._favorites is None:
|
|
146
176
|
error = "User item must be populated with favorites first."
|
|
147
177
|
raise UnpopulatedPropertyError(error)
|
|
@@ -210,12 +240,12 @@ class UserItem(object):
|
|
|
210
240
|
self._domain_name = domain_name
|
|
211
241
|
|
|
212
242
|
@classmethod
|
|
213
|
-
def from_response(cls, resp, ns) ->
|
|
243
|
+
def from_response(cls, resp, ns) -> list["UserItem"]:
|
|
214
244
|
element_name = ".//t:user"
|
|
215
245
|
return cls._parse_xml(element_name, resp, ns)
|
|
216
246
|
|
|
217
247
|
@classmethod
|
|
218
|
-
def from_response_as_owner(cls, resp, ns) ->
|
|
248
|
+
def from_response_as_owner(cls, resp, ns) -> list["UserItem"]:
|
|
219
249
|
element_name = ".//t:owner"
|
|
220
250
|
return cls._parse_xml(element_name, resp, ns)
|
|
221
251
|
|
|
@@ -283,7 +313,7 @@ class UserItem(object):
|
|
|
283
313
|
domain_name,
|
|
284
314
|
)
|
|
285
315
|
|
|
286
|
-
class CSVImport
|
|
316
|
+
class CSVImport:
|
|
287
317
|
"""
|
|
288
318
|
This class includes hardcoded options and logic for the CSV file format defined for user import
|
|
289
319
|
https://help.tableau.com/current/server/en-us/users_import.htm
|
|
@@ -308,7 +338,7 @@ class UserItem(object):
|
|
|
308
338
|
if line is None or line is False or line == "\n" or line == "":
|
|
309
339
|
return None
|
|
310
340
|
line = line.strip().lower()
|
|
311
|
-
values:
|
|
341
|
+
values: list[str] = list(map(str.strip, line.split(",")))
|
|
312
342
|
user = UserItem(values[UserItem.CSVImport.ColumnType.USERNAME])
|
|
313
343
|
if len(values) > 1:
|
|
314
344
|
if len(values) > UserItem.CSVImport.ColumnType.MAX:
|
|
@@ -337,7 +367,7 @@ class UserItem(object):
|
|
|
337
367
|
# Read through an entire CSV file meant for user import
|
|
338
368
|
# Return the number of valid lines and a list of all the invalid lines
|
|
339
369
|
@staticmethod
|
|
340
|
-
def validate_file_for_import(csv_file: io.TextIOWrapper, logger) ->
|
|
370
|
+
def validate_file_for_import(csv_file: io.TextIOWrapper, logger) -> tuple[int, list[str]]:
|
|
341
371
|
num_valid_lines = 0
|
|
342
372
|
invalid_lines = []
|
|
343
373
|
csv_file.seek(0) # set to start of file in case it has been read earlier
|
|
@@ -345,11 +375,11 @@ class UserItem(object):
|
|
|
345
375
|
while line and line != "":
|
|
346
376
|
try:
|
|
347
377
|
# do not print passwords
|
|
348
|
-
logger.info("Reading user {
|
|
378
|
+
logger.info(f"Reading user {line[:4]}")
|
|
349
379
|
UserItem.CSVImport._validate_import_line_or_throw(line, logger)
|
|
350
380
|
num_valid_lines += 1
|
|
351
381
|
except Exception as exc:
|
|
352
|
-
logger.info("Error parsing {
|
|
382
|
+
logger.info(f"Error parsing {line[:4]}: {exc}")
|
|
353
383
|
invalid_lines.append(line)
|
|
354
384
|
line = csv_file.readline()
|
|
355
385
|
return num_valid_lines, invalid_lines
|
|
@@ -358,7 +388,7 @@ class UserItem(object):
|
|
|
358
388
|
# Iterate through each field and validate the given value against hardcoded constraints
|
|
359
389
|
@staticmethod
|
|
360
390
|
def _validate_import_line_or_throw(incoming, logger) -> None:
|
|
361
|
-
_valid_attributes:
|
|
391
|
+
_valid_attributes: list[list[str]] = [
|
|
362
392
|
[],
|
|
363
393
|
[],
|
|
364
394
|
[],
|
|
@@ -373,23 +403,23 @@ class UserItem(object):
|
|
|
373
403
|
if len(line) > UserItem.CSVImport.ColumnType.MAX:
|
|
374
404
|
raise AttributeError("Too many attributes in line")
|
|
375
405
|
username = line[UserItem.CSVImport.ColumnType.USERNAME.value]
|
|
376
|
-
logger.debug("> details - {}"
|
|
406
|
+
logger.debug(f"> details - {username}")
|
|
377
407
|
UserItem.validate_username_or_throw(username)
|
|
378
408
|
for i in range(1, len(line)):
|
|
379
|
-
logger.debug("column {
|
|
409
|
+
logger.debug(f"column {UserItem.CSVImport.ColumnType(i).name}: {line[i]}")
|
|
380
410
|
UserItem.CSVImport._validate_attribute_value(
|
|
381
411
|
line[i], _valid_attributes[i], UserItem.CSVImport.ColumnType(i)
|
|
382
412
|
)
|
|
383
413
|
|
|
384
414
|
# Given a restricted set of possible values, confirm the item is in that set
|
|
385
415
|
@staticmethod
|
|
386
|
-
def _validate_attribute_value(item: str, possible_values:
|
|
416
|
+
def _validate_attribute_value(item: str, possible_values: list[str], column_type) -> None:
|
|
387
417
|
if item is None or item == "":
|
|
388
418
|
# value can be empty for any column except user, which is checked elsewhere
|
|
389
419
|
return
|
|
390
420
|
if item in possible_values or possible_values == []:
|
|
391
421
|
return
|
|
392
|
-
raise AttributeError("Invalid value {} for {}"
|
|
422
|
+
raise AttributeError(f"Invalid value {item} for {column_type}")
|
|
393
423
|
|
|
394
424
|
# https://help.tableau.com/current/server/en-us/csvguidelines.htm#settings_and_site_roles
|
|
395
425
|
# This logic is hardcoded to match the existing rules for import csv files
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
from requests import Response
|
|
4
|
-
from typing import Callable,
|
|
4
|
+
from typing import Callable, Optional
|
|
5
|
+
from collections.abc import Iterator
|
|
5
6
|
|
|
6
7
|
from defusedxml.ElementTree import fromstring
|
|
7
8
|
|
|
@@ -11,13 +12,13 @@ from .permissions_item import PermissionsRule
|
|
|
11
12
|
from .tag_item import TagItem
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
class ViewItem
|
|
15
|
+
class ViewItem:
|
|
15
16
|
def __init__(self) -> None:
|
|
16
17
|
self._content_url: Optional[str] = None
|
|
17
18
|
self._created_at: Optional[datetime] = None
|
|
18
19
|
self._id: Optional[str] = None
|
|
19
20
|
self._image: Optional[Callable[[], bytes]] = None
|
|
20
|
-
self._initial_tags:
|
|
21
|
+
self._initial_tags: set[str] = set()
|
|
21
22
|
self._name: Optional[str] = None
|
|
22
23
|
self._owner_id: Optional[str] = None
|
|
23
24
|
self._preview_image: Optional[Callable[[], bytes]] = None
|
|
@@ -29,15 +30,15 @@ class ViewItem(object):
|
|
|
29
30
|
self._sheet_type: Optional[str] = None
|
|
30
31
|
self._updated_at: Optional[datetime] = None
|
|
31
32
|
self._workbook_id: Optional[str] = None
|
|
32
|
-
self._permissions: Optional[Callable[[],
|
|
33
|
-
self.tags:
|
|
33
|
+
self._permissions: Optional[Callable[[], list[PermissionsRule]]] = None
|
|
34
|
+
self.tags: set[str] = set()
|
|
34
35
|
self._data_acceleration_config = {
|
|
35
36
|
"acceleration_enabled": None,
|
|
36
37
|
"acceleration_status": None,
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
def __str__(self):
|
|
40
|
-
return "<ViewItem {
|
|
41
|
+
return "<ViewItem {} '{}' contentUrl='{}' project={}>".format(
|
|
41
42
|
self._id, self.name, self.content_url, self.project_id
|
|
42
43
|
)
|
|
43
44
|
|
|
@@ -146,21 +147,21 @@ class ViewItem(object):
|
|
|
146
147
|
self._data_acceleration_config = value
|
|
147
148
|
|
|
148
149
|
@property
|
|
149
|
-
def permissions(self) ->
|
|
150
|
+
def permissions(self) -> list[PermissionsRule]:
|
|
150
151
|
if self._permissions is None:
|
|
151
152
|
error = "View item must be populated with permissions first."
|
|
152
153
|
raise UnpopulatedPropertyError(error)
|
|
153
154
|
return self._permissions()
|
|
154
155
|
|
|
155
|
-
def _set_permissions(self, permissions: Callable[[],
|
|
156
|
+
def _set_permissions(self, permissions: Callable[[], list[PermissionsRule]]) -> None:
|
|
156
157
|
self._permissions = permissions
|
|
157
158
|
|
|
158
159
|
@classmethod
|
|
159
|
-
def from_response(cls, resp: "Response", ns, workbook_id="") ->
|
|
160
|
+
def from_response(cls, resp: "Response", ns, workbook_id="") -> list["ViewItem"]:
|
|
160
161
|
return cls.from_xml_element(fromstring(resp), ns, workbook_id)
|
|
161
162
|
|
|
162
163
|
@classmethod
|
|
163
|
-
def from_xml_element(cls, parsed_response, ns, workbook_id="") ->
|
|
164
|
+
def from_xml_element(cls, parsed_response, ns, workbook_id="") -> list["ViewItem"]:
|
|
164
165
|
all_view_items = list()
|
|
165
166
|
all_view_xml = parsed_response.findall(".//t:view", namespaces=ns)
|
|
166
167
|
for view_xml in all_view_xml:
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
import json
|
|
3
|
+
from typing import Callable, Optional
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from xml.etree.ElementTree import Element
|
|
6
|
+
|
|
7
|
+
from defusedxml.ElementTree import fromstring
|
|
8
|
+
|
|
9
|
+
from tableauserverclient.datetime_helpers import parse_datetime
|
|
10
|
+
from tableauserverclient.models.connection_item import ConnectionItem
|
|
11
|
+
from tableauserverclient.models.exceptions import UnpopulatedPropertyError
|
|
12
|
+
from tableauserverclient.models.permissions_item import PermissionsRule
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class VirtualConnectionItem:
|
|
16
|
+
def __init__(self, name: str) -> None:
|
|
17
|
+
self.name = name
|
|
18
|
+
self.created_at: Optional[dt.datetime] = None
|
|
19
|
+
self.has_extracts: Optional[bool] = None
|
|
20
|
+
self._id: Optional[str] = None
|
|
21
|
+
self.is_certified: Optional[bool] = None
|
|
22
|
+
self.updated_at: Optional[dt.datetime] = None
|
|
23
|
+
self.webpage_url: Optional[str] = None
|
|
24
|
+
self._connections: Optional[Callable[[], Iterable[ConnectionItem]]] = None
|
|
25
|
+
self.project_id: Optional[str] = None
|
|
26
|
+
self.owner_id: Optional[str] = None
|
|
27
|
+
self.content: Optional[dict[str, dict]] = None
|
|
28
|
+
self.certification_note: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
return f"{self.__class__.__qualname__}(name={self.name})"
|
|
32
|
+
|
|
33
|
+
def __repr__(self) -> str:
|
|
34
|
+
return f"<{self!s}>"
|
|
35
|
+
|
|
36
|
+
def _set_permissions(self, permissions):
|
|
37
|
+
self._permissions = permissions
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def id(self) -> Optional[str]:
|
|
41
|
+
return self._id
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def permissions(self) -> list[PermissionsRule]:
|
|
45
|
+
if self._permissions is None:
|
|
46
|
+
error = "Workbook item must be populated with permissions first."
|
|
47
|
+
raise UnpopulatedPropertyError(error)
|
|
48
|
+
return self._permissions()
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def connections(self) -> Iterable[ConnectionItem]:
|
|
52
|
+
if self._connections is None:
|
|
53
|
+
raise AttributeError("connections not populated. Call populate_connections() first.")
|
|
54
|
+
return self._connections()
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_response(cls, response: bytes, ns: dict[str, str]) -> list["VirtualConnectionItem"]:
|
|
58
|
+
parsed_response = fromstring(response)
|
|
59
|
+
return [cls.from_xml(xml, ns) for xml in parsed_response.findall(".//t:virtualConnection[@name]", ns)]
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_xml(cls, xml: Element, ns: dict[str, str]) -> "VirtualConnectionItem":
|
|
63
|
+
v_conn = cls(xml.get("name", ""))
|
|
64
|
+
v_conn._id = xml.get("id", None)
|
|
65
|
+
v_conn.webpage_url = xml.get("webpageUrl", None)
|
|
66
|
+
v_conn.created_at = parse_datetime(xml.get("createdAt", None))
|
|
67
|
+
v_conn.updated_at = parse_datetime(xml.get("updatedAt", None))
|
|
68
|
+
v_conn.is_certified = string_to_bool(s) if (s := xml.get("isCertified", None)) else None
|
|
69
|
+
v_conn.certification_note = xml.get("certificationNote", None)
|
|
70
|
+
v_conn.has_extracts = string_to_bool(s) if (s := xml.get("hasExtracts", None)) else None
|
|
71
|
+
v_conn.project_id = p.get("id", None) if ((p := xml.find(".//t:project[@id]", ns)) is not None) else None
|
|
72
|
+
v_conn.owner_id = o.get("id", None) if ((o := xml.find(".//t:owner[@id]", ns)) is not None) else None
|
|
73
|
+
v_conn.content = json.loads(c.text or "{}") if ((c := xml.find(".//t:content", ns)) is not None) else None
|
|
74
|
+
return v_conn
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def string_to_bool(s: str) -> bool:
|
|
78
|
+
return s.lower() in ["true", "1", "t", "y", "yes"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import xml.etree.ElementTree as ET
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Optional
|
|
4
4
|
|
|
5
5
|
from defusedxml.ElementTree import fromstring
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ def _parse_event(events):
|
|
|
13
13
|
return NAMESPACE_RE.sub("", event.tag)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class WebhookItem
|
|
16
|
+
class WebhookItem:
|
|
17
17
|
def __init__(self):
|
|
18
18
|
self._id: Optional[str] = None
|
|
19
19
|
self.name: Optional[str] = None
|
|
@@ -45,10 +45,10 @@ class WebhookItem(object):
|
|
|
45
45
|
|
|
46
46
|
@event.setter
|
|
47
47
|
def event(self, value: str) -> None:
|
|
48
|
-
self._event = "webhook-source-event-{}"
|
|
48
|
+
self._event = f"webhook-source-event-{value}"
|
|
49
49
|
|
|
50
50
|
@classmethod
|
|
51
|
-
def from_response(cls:
|
|
51
|
+
def from_response(cls: type["WebhookItem"], resp: bytes, ns) -> list["WebhookItem"]:
|
|
52
52
|
all_webhooks_items = list()
|
|
53
53
|
parsed_response = fromstring(resp)
|
|
54
54
|
all_webhooks_xml = parsed_response.findall(".//t:webhook", namespaces=ns)
|
|
@@ -61,7 +61,7 @@ class WebhookItem(object):
|
|
|
61
61
|
return all_webhooks_items
|
|
62
62
|
|
|
63
63
|
@staticmethod
|
|
64
|
-
def _parse_element(webhook_xml: ET.Element, ns) ->
|
|
64
|
+
def _parse_element(webhook_xml: ET.Element, ns) -> tuple:
|
|
65
65
|
id = webhook_xml.get("id", None)
|
|
66
66
|
name = webhook_xml.get("name", None)
|
|
67
67
|
|
|
@@ -82,4 +82,4 @@ class WebhookItem(object):
|
|
|
82
82
|
return id, name, url, event, owner_id
|
|
83
83
|
|
|
84
84
|
def __repr__(self) -> str:
|
|
85
|
-
return "<Webhook id={} name={} url={} event={}>"
|
|
85
|
+
return f"<Webhook id={self.id} name={self.name} url={self.url} event={self.event}>"
|
|
@@ -2,7 +2,7 @@ import copy
|
|
|
2
2
|
import datetime
|
|
3
3
|
import uuid
|
|
4
4
|
import xml.etree.ElementTree as ET
|
|
5
|
-
from typing import Callable,
|
|
5
|
+
from typing import Callable, Optional
|
|
6
6
|
|
|
7
7
|
from defusedxml.ElementTree import fromstring
|
|
8
8
|
|
|
@@ -20,7 +20,85 @@ from .view_item import ViewItem
|
|
|
20
20
|
from .data_freshness_policy_item import DataFreshnessPolicyItem
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
class WorkbookItem
|
|
23
|
+
class WorkbookItem:
|
|
24
|
+
"""
|
|
25
|
+
The workbook resources for Tableau are defined in the WorkbookItem class.
|
|
26
|
+
The class corresponds to the workbook resources you can access using the
|
|
27
|
+
Tableau REST API. Some workbook methods take an instance of the WorkbookItem
|
|
28
|
+
class as arguments. The workbook item specifies the project.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
project_id : Optional[str], optional
|
|
33
|
+
The project ID for the workbook, by default None.
|
|
34
|
+
|
|
35
|
+
name : Optional[str], optional
|
|
36
|
+
The name of the workbook, by default None.
|
|
37
|
+
|
|
38
|
+
show_tabs : bool, optional
|
|
39
|
+
Determines whether the workbook shows tabs for the view.
|
|
40
|
+
|
|
41
|
+
Attributes
|
|
42
|
+
----------
|
|
43
|
+
connections : list[ConnectionItem]
|
|
44
|
+
The list of data connections (ConnectionItem) for the data sources used
|
|
45
|
+
by the workbook. You must first call the workbooks.populate_connections
|
|
46
|
+
method to access this data. See the ConnectionItem class.
|
|
47
|
+
|
|
48
|
+
content_url : Optional[str]
|
|
49
|
+
The name of the workbook as it appears in the URL.
|
|
50
|
+
|
|
51
|
+
created_at : Optional[datetime.datetime]
|
|
52
|
+
The date and time the workbook was created.
|
|
53
|
+
|
|
54
|
+
description : Optional[str]
|
|
55
|
+
User-defined description of the workbook.
|
|
56
|
+
|
|
57
|
+
id : Optional[str]
|
|
58
|
+
The identifier for the workbook. You need this value to query a specific
|
|
59
|
+
workbook or to delete a workbook with the get_by_id and delete methods.
|
|
60
|
+
|
|
61
|
+
owner_id : Optional[str]
|
|
62
|
+
The identifier for the owner (UserItem) of the workbook.
|
|
63
|
+
|
|
64
|
+
preview_image : bytes
|
|
65
|
+
The thumbnail image for the view. You must first call the
|
|
66
|
+
workbooks.populate_preview_image method to access this data.
|
|
67
|
+
|
|
68
|
+
project_name : Optional[str]
|
|
69
|
+
The name of the project that contains the workbook.
|
|
70
|
+
|
|
71
|
+
size: int
|
|
72
|
+
The size of the workbook in megabytes.
|
|
73
|
+
|
|
74
|
+
hidden_views: Optional[list[str]]
|
|
75
|
+
List of string names of views that need to be hidden when the workbook
|
|
76
|
+
is published.
|
|
77
|
+
|
|
78
|
+
tags: set[str]
|
|
79
|
+
The set of tags associated with the workbook.
|
|
80
|
+
|
|
81
|
+
updated_at : Optional[datetime.datetime]
|
|
82
|
+
The date and time the workbook was last updated.
|
|
83
|
+
|
|
84
|
+
views : list[ViewItem]
|
|
85
|
+
The list of views (ViewItem) for the workbook. You must first call the
|
|
86
|
+
workbooks.populate_views method to access this data. See the ViewItem
|
|
87
|
+
class.
|
|
88
|
+
|
|
89
|
+
web_page_url : Optional[str]
|
|
90
|
+
The full URL for the workbook.
|
|
91
|
+
|
|
92
|
+
Examples
|
|
93
|
+
--------
|
|
94
|
+
# creating a new instance of a WorkbookItem
|
|
95
|
+
>>> import tableauserverclient as TSC
|
|
96
|
+
|
|
97
|
+
>>> # Create new workbook_item with project id '3a8b6148-493c-11e6-a621-6f3499394a39'
|
|
98
|
+
|
|
99
|
+
>>> new_workbook = TSC.WorkbookItem('3a8b6148-493c-11e6-a621-6f3499394a39')
|
|
100
|
+
"""
|
|
101
|
+
|
|
24
102
|
def __init__(self, project_id: Optional[str] = None, name: Optional[str] = None, show_tabs: bool = False) -> None:
|
|
25
103
|
self._connections = None
|
|
26
104
|
self._content_url = None
|
|
@@ -35,15 +113,15 @@ class WorkbookItem(object):
|
|
|
35
113
|
self._revisions = None
|
|
36
114
|
self._size = None
|
|
37
115
|
self._updated_at = None
|
|
38
|
-
self._views: Optional[Callable[[],
|
|
116
|
+
self._views: Optional[Callable[[], list[ViewItem]]] = None
|
|
39
117
|
self.name = name
|
|
40
118
|
self._description = None
|
|
41
119
|
self.owner_id: Optional[str] = None
|
|
42
120
|
# workaround for Personal Space workbooks without a project
|
|
43
121
|
self.project_id: Optional[str] = project_id or uuid.uuid4().__str__()
|
|
44
122
|
self.show_tabs = show_tabs
|
|
45
|
-
self.hidden_views: Optional[
|
|
46
|
-
self.tags:
|
|
123
|
+
self.hidden_views: Optional[list[str]] = None
|
|
124
|
+
self.tags: set[str] = set()
|
|
47
125
|
self.data_acceleration_config = {
|
|
48
126
|
"acceleration_enabled": None,
|
|
49
127
|
"accelerate_now": None,
|
|
@@ -56,7 +134,7 @@ class WorkbookItem(object):
|
|
|
56
134
|
return None
|
|
57
135
|
|
|
58
136
|
def __str__(self):
|
|
59
|
-
return "<WorkbookItem {
|
|
137
|
+
return "<WorkbookItem {} '{}' contentUrl='{}' project={}>".format(
|
|
60
138
|
self._id, self.name, self.content_url, self.project_id
|
|
61
139
|
)
|
|
62
140
|
|
|
@@ -64,14 +142,14 @@ class WorkbookItem(object):
|
|
|
64
142
|
return self.__str__() + " { " + ", ".join(" % s: % s" % item for item in vars(self).items()) + "}"
|
|
65
143
|
|
|
66
144
|
@property
|
|
67
|
-
def connections(self) ->
|
|
145
|
+
def connections(self) -> list[ConnectionItem]:
|
|
68
146
|
if self._connections is None:
|
|
69
147
|
error = "Workbook item must be populated with connections first."
|
|
70
148
|
raise UnpopulatedPropertyError(error)
|
|
71
149
|
return self._connections()
|
|
72
150
|
|
|
73
151
|
@property
|
|
74
|
-
def permissions(self) ->
|
|
152
|
+
def permissions(self) -> list[PermissionsRule]:
|
|
75
153
|
if self._permissions is None:
|
|
76
154
|
error = "Workbook item must be populated with permissions first."
|
|
77
155
|
raise UnpopulatedPropertyError(error)
|
|
@@ -152,7 +230,7 @@ class WorkbookItem(object):
|
|
|
152
230
|
return self._updated_at
|
|
153
231
|
|
|
154
232
|
@property
|
|
155
|
-
def views(self) ->
|
|
233
|
+
def views(self) -> list[ViewItem]:
|
|
156
234
|
# Views can be set in an initial workbook response OR by a call
|
|
157
235
|
# to Server. Without getting too fancy, I think we can rely on
|
|
158
236
|
# returning a list from the response, until they call
|
|
@@ -191,7 +269,7 @@ class WorkbookItem(object):
|
|
|
191
269
|
self._data_freshness_policy = value
|
|
192
270
|
|
|
193
271
|
@property
|
|
194
|
-
def revisions(self) ->
|
|
272
|
+
def revisions(self) -> list[RevisionItem]:
|
|
195
273
|
if self._revisions is None:
|
|
196
274
|
error = "Workbook item must be populated with revisions first."
|
|
197
275
|
raise UnpopulatedPropertyError(error)
|
|
@@ -203,7 +281,7 @@ class WorkbookItem(object):
|
|
|
203
281
|
def _set_permissions(self, permissions):
|
|
204
282
|
self._permissions = permissions
|
|
205
283
|
|
|
206
|
-
def _set_views(self, views: Callable[[],
|
|
284
|
+
def _set_views(self, views: Callable[[], list[ViewItem]]) -> None:
|
|
207
285
|
self._views = views
|
|
208
286
|
|
|
209
287
|
def _set_pdf(self, pdf: Callable[[], bytes]) -> None:
|
|
@@ -316,7 +394,7 @@ class WorkbookItem(object):
|
|
|
316
394
|
self.data_freshness_policy = data_freshness_policy
|
|
317
395
|
|
|
318
396
|
@classmethod
|
|
319
|
-
def from_response(cls, resp: str, ns:
|
|
397
|
+
def from_response(cls, resp: str, ns: dict[str, str]) -> list["WorkbookItem"]:
|
|
320
398
|
all_workbook_items = list()
|
|
321
399
|
parsed_response = fromstring(resp)
|
|
322
400
|
all_workbook_xml = parsed_response.findall(".//t:workbook", namespaces=ns)
|
tableauserverclient/namespace.py
CHANGED
|
@@ -11,7 +11,7 @@ from tableauserverclient.server.filter import Filter
|
|
|
11
11
|
from tableauserverclient.server.sort import Sort
|
|
12
12
|
from tableauserverclient.server.server import Server
|
|
13
13
|
from tableauserverclient.server.pager import Pager
|
|
14
|
-
from tableauserverclient.server.endpoint.exceptions import NotSignedInError
|
|
14
|
+
from tableauserverclient.server.endpoint.exceptions import FailedSignInError, NotSignedInError
|
|
15
15
|
|
|
16
16
|
from tableauserverclient.server.endpoint import (
|
|
17
17
|
Auth,
|
|
@@ -57,6 +57,7 @@ __all__ = [
|
|
|
57
57
|
"Sort",
|
|
58
58
|
"Server",
|
|
59
59
|
"Pager",
|
|
60
|
+
"FailedSignInError",
|
|
60
61
|
"NotSignedInError",
|
|
61
62
|
"Auth",
|
|
62
63
|
"CustomViews",
|
|
@@ -12,7 +12,9 @@ from tableauserverclient.server.endpoint.flow_runs_endpoint import FlowRuns
|
|
|
12
12
|
from tableauserverclient.server.endpoint.flows_endpoint import Flows
|
|
13
13
|
from tableauserverclient.server.endpoint.flow_task_endpoint import FlowTasks
|
|
14
14
|
from tableauserverclient.server.endpoint.groups_endpoint import Groups
|
|
15
|
+
from tableauserverclient.server.endpoint.groupsets_endpoint import GroupSets
|
|
15
16
|
from tableauserverclient.server.endpoint.jobs_endpoint import Jobs
|
|
17
|
+
from tableauserverclient.server.endpoint.linked_tasks_endpoint import LinkedTasks
|
|
16
18
|
from tableauserverclient.server.endpoint.metadata_endpoint import Metadata
|
|
17
19
|
from tableauserverclient.server.endpoint.metrics_endpoint import Metrics
|
|
18
20
|
from tableauserverclient.server.endpoint.projects_endpoint import Projects
|
|
@@ -21,9 +23,11 @@ from tableauserverclient.server.endpoint.server_info_endpoint import ServerInfo
|
|
|
21
23
|
from tableauserverclient.server.endpoint.sites_endpoint import Sites
|
|
22
24
|
from tableauserverclient.server.endpoint.subscriptions_endpoint import Subscriptions
|
|
23
25
|
from tableauserverclient.server.endpoint.tables_endpoint import Tables
|
|
26
|
+
from tableauserverclient.server.endpoint.resource_tagger import Tags
|
|
24
27
|
from tableauserverclient.server.endpoint.tasks_endpoint import Tasks
|
|
25
28
|
from tableauserverclient.server.endpoint.users_endpoint import Users
|
|
26
29
|
from tableauserverclient.server.endpoint.views_endpoint import Views
|
|
30
|
+
from tableauserverclient.server.endpoint.virtual_connections_endpoint import VirtualConnections
|
|
27
31
|
from tableauserverclient.server.endpoint.webhooks_endpoint import Webhooks
|
|
28
32
|
from tableauserverclient.server.endpoint.workbooks_endpoint import Workbooks
|
|
29
33
|
|
|
@@ -43,7 +47,9 @@ __all__ = [
|
|
|
43
47
|
"Flows",
|
|
44
48
|
"FlowTasks",
|
|
45
49
|
"Groups",
|
|
50
|
+
"GroupSets",
|
|
46
51
|
"Jobs",
|
|
52
|
+
"LinkedTasks",
|
|
47
53
|
"Metadata",
|
|
48
54
|
"Metrics",
|
|
49
55
|
"Projects",
|
|
@@ -53,9 +59,11 @@ __all__ = [
|
|
|
53
59
|
"Sites",
|
|
54
60
|
"Subscriptions",
|
|
55
61
|
"Tables",
|
|
62
|
+
"Tags",
|
|
56
63
|
"Tasks",
|
|
57
64
|
"Users",
|
|
58
65
|
"Views",
|
|
66
|
+
"VirtualConnections",
|
|
59
67
|
"Webhooks",
|
|
60
68
|
"Workbooks",
|
|
61
69
|
]
|