tableauserverclient 0.25__py3-none-any.whl → 0.27.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 +42 -1
- tableauserverclient/_version.py +4 -4
- tableauserverclient/config.py +13 -0
- tableauserverclient/datetime_helpers.py +4 -0
- tableauserverclient/helpers/logging.py +4 -0
- tableauserverclient/models/__init__.py +1 -1
- tableauserverclient/models/column_item.py +3 -0
- tableauserverclient/models/connection_credentials.py +7 -0
- tableauserverclient/models/connection_item.py +1 -1
- tableauserverclient/models/custom_view_item.py +5 -0
- tableauserverclient/models/data_acceleration_report_item.py +3 -0
- tableauserverclient/models/datasource_item.py +10 -54
- tableauserverclient/models/favorites_item.py +56 -40
- tableauserverclient/models/fileupload_item.py +2 -2
- tableauserverclient/models/flow_item.py +30 -25
- tableauserverclient/models/group_item.py +1 -4
- tableauserverclient/models/interval_item.py +12 -0
- tableauserverclient/models/job_item.py +10 -1
- tableauserverclient/models/metric_item.py +36 -29
- tableauserverclient/models/pagination_item.py +3 -0
- tableauserverclient/models/permissions_item.py +8 -5
- tableauserverclient/models/project_item.py +11 -13
- tableauserverclient/models/schedule_item.py +6 -7
- tableauserverclient/models/server_info_item.py +2 -2
- tableauserverclient/models/site_item.py +3 -0
- tableauserverclient/models/subscription_item.py +8 -0
- tableauserverclient/models/table_item.py +6 -0
- tableauserverclient/models/tableau_auth.py +41 -6
- tableauserverclient/models/tableau_types.py +4 -2
- tableauserverclient/models/user_item.py +5 -1
- tableauserverclient/models/view_item.py +39 -36
- tableauserverclient/models/workbook_item.py +14 -43
- tableauserverclient/server/__init__.py +1 -3
- tableauserverclient/server/endpoint/__init__.py +1 -5
- tableauserverclient/server/endpoint/auth_endpoint.py +29 -8
- tableauserverclient/server/endpoint/custom_views_endpoint.py +1 -1
- tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py +1 -1
- tableauserverclient/server/endpoint/data_alert_endpoint.py +1 -1
- tableauserverclient/server/endpoint/databases_endpoint.py +1 -1
- tableauserverclient/server/endpoint/datasources_endpoint.py +21 -15
- tableauserverclient/server/endpoint/default_permissions_endpoint.py +1 -1
- tableauserverclient/server/endpoint/dqw_endpoint.py +1 -1
- tableauserverclient/server/endpoint/endpoint.py +98 -11
- tableauserverclient/server/endpoint/exceptions.py +1 -5
- tableauserverclient/server/endpoint/favorites_endpoint.py +71 -29
- tableauserverclient/server/endpoint/fileuploads_endpoint.py +11 -10
- tableauserverclient/server/endpoint/flow_runs_endpoint.py +1 -1
- tableauserverclient/server/endpoint/flows_endpoint.py +5 -5
- tableauserverclient/server/endpoint/groups_endpoint.py +5 -2
- tableauserverclient/server/endpoint/jobs_endpoint.py +1 -1
- tableauserverclient/server/endpoint/metadata_endpoint.py +1 -1
- tableauserverclient/server/endpoint/metrics_endpoint.py +1 -1
- tableauserverclient/server/endpoint/permissions_endpoint.py +1 -1
- tableauserverclient/server/endpoint/projects_endpoint.py +3 -1
- tableauserverclient/server/endpoint/resource_tagger.py +3 -3
- tableauserverclient/server/endpoint/schedules_endpoint.py +2 -1
- tableauserverclient/server/endpoint/server_info_endpoint.py +2 -4
- tableauserverclient/server/endpoint/sites_endpoint.py +1 -1
- tableauserverclient/server/endpoint/subscriptions_endpoint.py +1 -1
- tableauserverclient/server/endpoint/tables_endpoint.py +1 -1
- tableauserverclient/server/endpoint/tasks_endpoint.py +12 -1
- tableauserverclient/server/endpoint/users_endpoint.py +1 -1
- tableauserverclient/server/endpoint/views_endpoint.py +1 -1
- tableauserverclient/server/endpoint/webhooks_endpoint.py +1 -1
- tableauserverclient/server/endpoint/workbooks_endpoint.py +4 -2
- tableauserverclient/server/exceptions.py +8 -1
- tableauserverclient/server/filter.py +5 -1
- tableauserverclient/server/request_factory.py +56 -12
- tableauserverclient/server/request_options.py +4 -2
- tableauserverclient/server/server.py +12 -13
- {tableauserverclient-0.25.dist-info → tableauserverclient-0.27.post0.dev1.dist-info}/METADATA +12 -10
- tableauserverclient-0.27.post0.dev1.dist-info/RECORD +97 -0
- {tableauserverclient-0.25.dist-info → tableauserverclient-0.27.post0.dev1.dist-info}/WHEEL +1 -1
- tableauserverclient-0.25.dist-info/RECORD +0 -95
- {tableauserverclient-0.25.dist-info → tableauserverclient-0.27.post0.dev1.dist-info}/LICENSE +0 -0
- {tableauserverclient-0.25.dist-info → tableauserverclient-0.27.post0.dev1.dist-info}/LICENSE.versioneer +0 -0
- {tableauserverclient-0.25.dist-info → tableauserverclient-0.27.post0.dev1.dist-info}/top_level.txt +0 -0
|
@@ -5,6 +5,7 @@ from typing import List, Optional, Set
|
|
|
5
5
|
from tableauserverclient.datetime_helpers import parse_datetime
|
|
6
6
|
from .property_decorators import property_is_boolean, property_is_datetime
|
|
7
7
|
from .tag_item import TagItem
|
|
8
|
+
from .permissions_item import Permission
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class MetricItem(object):
|
|
@@ -22,6 +23,7 @@ class MetricItem(object):
|
|
|
22
23
|
self._view_id: Optional[str] = None
|
|
23
24
|
self._initial_tags: Set[str] = set()
|
|
24
25
|
self.tags: Set[str] = set()
|
|
26
|
+
self._permissions: Optional[Permission] = None
|
|
25
27
|
|
|
26
28
|
@property
|
|
27
29
|
def id(self) -> Optional[str]:
|
|
@@ -110,9 +112,15 @@ class MetricItem(object):
|
|
|
110
112
|
def view_id(self, value: Optional[str]) -> None:
|
|
111
113
|
self._view_id = value
|
|
112
114
|
|
|
113
|
-
def
|
|
115
|
+
def _set_permissions(self, permissions):
|
|
116
|
+
self._permissions = permissions
|
|
117
|
+
|
|
118
|
+
def __str__(self):
|
|
114
119
|
return "<MetricItem# name={_name} id={_id} owner_id={_owner_id}>".format(**vars(self))
|
|
115
120
|
|
|
121
|
+
def __repr__(self):
|
|
122
|
+
return self.__str__() + " { " + ", ".join(" % s: % s" % item for item in vars(self).items()) + "}"
|
|
123
|
+
|
|
116
124
|
@classmethod
|
|
117
125
|
def from_response(
|
|
118
126
|
cls,
|
|
@@ -123,36 +131,35 @@ class MetricItem(object):
|
|
|
123
131
|
parsed_response = ET.fromstring(resp)
|
|
124
132
|
all_metric_xml = parsed_response.findall(".//t:metric", namespaces=ns)
|
|
125
133
|
for metric_xml in all_metric_xml:
|
|
126
|
-
|
|
127
|
-
metric_item._id = metric_xml.get("id", None)
|
|
128
|
-
metric_item._name = metric_xml.get("name", None)
|
|
129
|
-
metric_item._description = metric_xml.get("description", None)
|
|
130
|
-
metric_item._webpage_url = metric_xml.get("webpageUrl", None)
|
|
131
|
-
metric_item._created_at = parse_datetime(metric_xml.get("createdAt", None))
|
|
132
|
-
metric_item._updated_at = parse_datetime(metric_xml.get("updatedAt", None))
|
|
133
|
-
metric_item._suspended = string_to_bool(metric_xml.get("suspended", ""))
|
|
134
|
-
for owner in metric_xml.findall(".//t:owner", namespaces=ns):
|
|
135
|
-
metric_item._owner_id = owner.get("id", None)
|
|
136
|
-
|
|
137
|
-
for project in metric_xml.findall(".//t:project", namespaces=ns):
|
|
138
|
-
metric_item._project_id = project.get("id", None)
|
|
139
|
-
metric_item._project_name = project.get("name", None)
|
|
140
|
-
|
|
141
|
-
for view in metric_xml.findall(".//t:underlyingView", namespaces=ns):
|
|
142
|
-
metric_item._view_id = view.get("id", None)
|
|
143
|
-
|
|
144
|
-
tags = set()
|
|
145
|
-
tags_elem = metric_xml.find(".//t:tags", namespaces=ns)
|
|
146
|
-
if tags_elem is not None:
|
|
147
|
-
all_tags = TagItem.from_xml_element(tags_elem, ns)
|
|
148
|
-
tags = all_tags
|
|
149
|
-
|
|
150
|
-
metric_item.tags = tags
|
|
151
|
-
metric_item._initial_tags = tags
|
|
152
|
-
|
|
153
|
-
all_metric_items.append(metric_item)
|
|
134
|
+
all_metric_items.append(cls.from_xml(metric_xml, ns))
|
|
154
135
|
return all_metric_items
|
|
155
136
|
|
|
137
|
+
@classmethod
|
|
138
|
+
def from_xml(cls, metric_xml, ns):
|
|
139
|
+
metric_item = cls()
|
|
140
|
+
metric_item._id = metric_xml.get("id", None)
|
|
141
|
+
metric_item._name = metric_xml.get("name", None)
|
|
142
|
+
metric_item._description = metric_xml.get("description", None)
|
|
143
|
+
metric_item._webpage_url = metric_xml.get("webpageUrl", None)
|
|
144
|
+
metric_item._created_at = parse_datetime(metric_xml.get("createdAt", None))
|
|
145
|
+
metric_item._updated_at = parse_datetime(metric_xml.get("updatedAt", None))
|
|
146
|
+
metric_item._suspended = string_to_bool(metric_xml.get("suspended", ""))
|
|
147
|
+
for owner in metric_xml.findall(".//t:owner", namespaces=ns):
|
|
148
|
+
metric_item._owner_id = owner.get("id", None)
|
|
149
|
+
for project in metric_xml.findall(".//t:project", namespaces=ns):
|
|
150
|
+
metric_item._project_id = project.get("id", None)
|
|
151
|
+
metric_item._project_name = project.get("name", None)
|
|
152
|
+
for view in metric_xml.findall(".//t:underlyingView", namespaces=ns):
|
|
153
|
+
metric_item._view_id = view.get("id", None)
|
|
154
|
+
tags = set()
|
|
155
|
+
tags_elem = metric_xml.find(".//t:tags", namespaces=ns)
|
|
156
|
+
if tags_elem is not None:
|
|
157
|
+
all_tags = TagItem.from_xml_element(tags_elem, ns)
|
|
158
|
+
tags = all_tags
|
|
159
|
+
metric_item.tags = tags
|
|
160
|
+
metric_item._initial_tags = tags
|
|
161
|
+
return metric_item
|
|
162
|
+
|
|
156
163
|
|
|
157
164
|
# Used to convert string represented boolean to a boolean type
|
|
158
165
|
def string_to_bool(s: str) -> bool:
|
|
@@ -7,6 +7,9 @@ class PaginationItem(object):
|
|
|
7
7
|
self._page_size = None
|
|
8
8
|
self._total_available = None
|
|
9
9
|
|
|
10
|
+
def __repr__(self):
|
|
11
|
+
return f"<PaginationItem page_number={self._page_number} page_size={self._page_size} total={self._total_available}>"
|
|
12
|
+
|
|
10
13
|
@property
|
|
11
14
|
def page_number(self) -> int:
|
|
12
15
|
return self._page_number
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
import xml.etree.ElementTree as ET
|
|
3
2
|
from typing import Dict, List, Optional
|
|
4
3
|
|
|
@@ -9,7 +8,7 @@ from .group_item import GroupItem
|
|
|
9
8
|
from .reference_item import ResourceReference
|
|
10
9
|
from .user_item import UserItem
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
from tableauserverclient.helpers.logging import logger
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
class Permission:
|
|
@@ -17,6 +16,9 @@ class Permission:
|
|
|
17
16
|
Allow = "Allow"
|
|
18
17
|
Deny = "Deny"
|
|
19
18
|
|
|
19
|
+
def __repr__(self):
|
|
20
|
+
return "<Enum Mode: Allow | Deny>"
|
|
21
|
+
|
|
20
22
|
class Capability:
|
|
21
23
|
AddComment = "AddComment"
|
|
22
24
|
ChangeHierarchy = "ChangeHierarchy"
|
|
@@ -39,17 +41,18 @@ class Permission:
|
|
|
39
41
|
CreateRefreshMetrics = "CreateRefreshMetrics"
|
|
40
42
|
SaveAs = "SaveAs"
|
|
41
43
|
|
|
44
|
+
def __repr__(self):
|
|
45
|
+
return "<Enum Capability: AddComment | ChangeHierarchy | ChangePermission ... (17 more) >"
|
|
46
|
+
|
|
42
47
|
|
|
43
48
|
class PermissionsRule(object):
|
|
44
49
|
def __init__(self, grantee: ResourceReference, capabilities: Dict[str, str]) -> None:
|
|
45
50
|
self.grantee = grantee
|
|
46
51
|
self.capabilities = capabilities
|
|
47
52
|
|
|
48
|
-
def
|
|
53
|
+
def __repr__(self):
|
|
49
54
|
return "<PermissionsRule grantee={}, capabilities={}>".format(self.grantee, self.capabilities)
|
|
50
55
|
|
|
51
|
-
__repr__ = __str__
|
|
52
|
-
|
|
53
56
|
@classmethod
|
|
54
57
|
def from_response(cls, resp, ns=None) -> List["PermissionsRule"]:
|
|
55
58
|
parsed_response = fromstring(resp)
|
|
@@ -21,10 +21,11 @@ class ProjectItem(object):
|
|
|
21
21
|
|
|
22
22
|
def __init__(
|
|
23
23
|
self,
|
|
24
|
-
name: str,
|
|
24
|
+
name: Optional[str] = None,
|
|
25
25
|
description: Optional[str] = None,
|
|
26
26
|
content_permissions: Optional[str] = None,
|
|
27
27
|
parent_id: Optional[str] = None,
|
|
28
|
+
samples: Optional[bool] = None,
|
|
28
29
|
) -> None:
|
|
29
30
|
self._content_permissions = None
|
|
30
31
|
self._id: Optional[str] = None
|
|
@@ -32,6 +33,7 @@ class ProjectItem(object):
|
|
|
32
33
|
self.name: str = name
|
|
33
34
|
self.content_permissions: Optional[str] = content_permissions
|
|
34
35
|
self.parent_id: Optional[str] = parent_id
|
|
36
|
+
self._samples: Optional[bool] = samples
|
|
35
37
|
|
|
36
38
|
self._permissions = None
|
|
37
39
|
self._default_workbook_permissions = None
|
|
@@ -104,11 +106,10 @@ class ProjectItem(object):
|
|
|
104
106
|
return self._id
|
|
105
107
|
|
|
106
108
|
@property
|
|
107
|
-
def name(self) -> str:
|
|
109
|
+
def name(self) -> Optional[str]:
|
|
108
110
|
return self._name
|
|
109
111
|
|
|
110
112
|
@name.setter
|
|
111
|
-
@property_not_empty
|
|
112
113
|
def name(self, value: str) -> None:
|
|
113
114
|
self._name = value
|
|
114
115
|
|
|
@@ -173,19 +174,16 @@ class ProjectItem(object):
|
|
|
173
174
|
all_project_xml = parsed_response.findall(".//t:project", namespaces=ns)
|
|
174
175
|
|
|
175
176
|
for project_xml in all_project_xml:
|
|
176
|
-
(
|
|
177
|
-
id,
|
|
178
|
-
name,
|
|
179
|
-
description,
|
|
180
|
-
content_permissions,
|
|
181
|
-
parent_id,
|
|
182
|
-
owner_id,
|
|
183
|
-
) = cls._parse_element(project_xml)
|
|
184
|
-
project_item = cls(name)
|
|
185
|
-
project_item._set_values(id, name, description, content_permissions, parent_id, owner_id)
|
|
177
|
+
project_item = cls.from_xml(project_xml)
|
|
186
178
|
all_project_items.append(project_item)
|
|
187
179
|
return all_project_items
|
|
188
180
|
|
|
181
|
+
@classmethod
|
|
182
|
+
def from_xml(cls, project_xml, namespace=None) -> "ProjectItem":
|
|
183
|
+
project_item = cls()
|
|
184
|
+
project_item._set_values(*cls._parse_element(project_xml))
|
|
185
|
+
return project_item
|
|
186
|
+
|
|
189
187
|
@staticmethod
|
|
190
188
|
def _parse_element(project_xml):
|
|
191
189
|
id = project_xml.get("id", None)
|
|
@@ -14,8 +14,6 @@ from .interval_item import (
|
|
|
14
14
|
)
|
|
15
15
|
from .property_decorators import (
|
|
16
16
|
property_is_enum,
|
|
17
|
-
property_not_nullable,
|
|
18
|
-
property_is_int,
|
|
19
17
|
)
|
|
20
18
|
|
|
21
19
|
Interval = Union[HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval]
|
|
@@ -27,6 +25,7 @@ class ScheduleItem(object):
|
|
|
27
25
|
Flow = "Flow"
|
|
28
26
|
Subscription = "Subscription"
|
|
29
27
|
DataAcceleration = "DataAcceleration"
|
|
28
|
+
ActiveDirectorySync = "ActiveDirectorySync"
|
|
30
29
|
|
|
31
30
|
class ExecutionOrder:
|
|
32
31
|
Parallel = "Parallel"
|
|
@@ -49,9 +48,12 @@ class ScheduleItem(object):
|
|
|
49
48
|
self.priority: int = priority
|
|
50
49
|
self.schedule_type: str = schedule_type
|
|
51
50
|
|
|
52
|
-
def
|
|
51
|
+
def __str__(self):
|
|
53
52
|
return '<Schedule#{_id} "{_name}" {interval_item}>'.format(**vars(self))
|
|
54
53
|
|
|
54
|
+
def __repr__(self):
|
|
55
|
+
return self.__str__() + " { " + ", ".join(" % s: % s" % item for item in vars(self).items()) + "}"
|
|
56
|
+
|
|
55
57
|
@property
|
|
56
58
|
def created_at(self) -> Optional[datetime]:
|
|
57
59
|
return self._created_at
|
|
@@ -74,11 +76,10 @@ class ScheduleItem(object):
|
|
|
74
76
|
return self._id
|
|
75
77
|
|
|
76
78
|
@property
|
|
77
|
-
def name(self) -> str:
|
|
79
|
+
def name(self) -> Optional[str]:
|
|
78
80
|
return self._name
|
|
79
81
|
|
|
80
82
|
@name.setter
|
|
81
|
-
@property_not_nullable
|
|
82
83
|
def name(self, value: str):
|
|
83
84
|
self._name = value
|
|
84
85
|
|
|
@@ -91,7 +92,6 @@ class ScheduleItem(object):
|
|
|
91
92
|
return self._priority
|
|
92
93
|
|
|
93
94
|
@priority.setter
|
|
94
|
-
@property_is_int(range=(1, 100))
|
|
95
95
|
def priority(self, value: int):
|
|
96
96
|
self._priority = value
|
|
97
97
|
|
|
@@ -101,7 +101,6 @@ class ScheduleItem(object):
|
|
|
101
101
|
|
|
102
102
|
@schedule_type.setter
|
|
103
103
|
@property_is_enum(Type)
|
|
104
|
-
@property_not_nullable
|
|
105
104
|
def schedule_type(self, value: str):
|
|
106
105
|
self._schedule_type = value
|
|
107
106
|
|
|
@@ -3,6 +3,7 @@ import warnings
|
|
|
3
3
|
import xml
|
|
4
4
|
|
|
5
5
|
from defusedxml.ElementTree import fromstring
|
|
6
|
+
from tableauserverclient.helpers.logging import logger
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class ServerInfoItem(object):
|
|
@@ -11,7 +12,7 @@ class ServerInfoItem(object):
|
|
|
11
12
|
self._build_number = build_number
|
|
12
13
|
self._rest_api_version = rest_api_version
|
|
13
14
|
|
|
14
|
-
def
|
|
15
|
+
def __repr__(self):
|
|
15
16
|
return (
|
|
16
17
|
"ServerInfoItem: [product version: "
|
|
17
18
|
+ self._product_version
|
|
@@ -36,7 +37,6 @@ class ServerInfoItem(object):
|
|
|
36
37
|
|
|
37
38
|
@classmethod
|
|
38
39
|
def from_response(cls, resp, ns):
|
|
39
|
-
logger = logging.getLogger("TSC.ServerInfo")
|
|
40
40
|
try:
|
|
41
41
|
parsed_response = fromstring(resp)
|
|
42
42
|
except xml.etree.ElementTree.ParseError as error:
|
|
@@ -39,6 +39,9 @@ class SiteItem(object):
|
|
|
39
39
|
+ ">"
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
+
def __repr__(self):
|
|
43
|
+
return self.__str__() + " { " + ", ".join(" % s: % s" % item for item in vars(self).items()) + "}"
|
|
44
|
+
|
|
42
45
|
class AdminMode:
|
|
43
46
|
ContentAndUsers: str = "ContentAndUsers"
|
|
44
47
|
ContentOnly: str = "ContentOnly"
|
|
@@ -4,6 +4,7 @@ from defusedxml.ElementTree import fromstring
|
|
|
4
4
|
|
|
5
5
|
from .property_decorators import property_is_boolean
|
|
6
6
|
from .target import Target
|
|
7
|
+
from tableauserverclient.models import ScheduleItem
|
|
7
8
|
|
|
8
9
|
if TYPE_CHECKING:
|
|
9
10
|
from .target import Target
|
|
@@ -23,6 +24,7 @@ class SubscriptionItem(object):
|
|
|
23
24
|
self.suspended = False
|
|
24
25
|
self.target = target
|
|
25
26
|
self.user_id = user_id
|
|
27
|
+
self.schedule = None
|
|
26
28
|
|
|
27
29
|
def __repr__(self) -> str:
|
|
28
30
|
if self.id is not None:
|
|
@@ -92,9 +94,14 @@ class SubscriptionItem(object):
|
|
|
92
94
|
|
|
93
95
|
# Schedule element
|
|
94
96
|
schedule_id = None
|
|
97
|
+
schedule = None
|
|
95
98
|
if schedule_element is not None:
|
|
96
99
|
schedule_id = schedule_element.get("id", None)
|
|
97
100
|
|
|
101
|
+
# If schedule id is not provided, then TOL with full schedule provided
|
|
102
|
+
if schedule_id is None:
|
|
103
|
+
schedule = ScheduleItem.from_element(element, ns)
|
|
104
|
+
|
|
98
105
|
# Content element
|
|
99
106
|
target = None
|
|
100
107
|
send_if_view_empty = None
|
|
@@ -127,6 +134,7 @@ class SubscriptionItem(object):
|
|
|
127
134
|
sub.page_size_option = page_size_option
|
|
128
135
|
sub.send_if_view_empty = send_if_view_empty
|
|
129
136
|
sub.suspended = suspended
|
|
137
|
+
sub.schedule = schedule
|
|
130
138
|
|
|
131
139
|
return sub
|
|
132
140
|
|
|
@@ -19,6 +19,12 @@ class TableItem(object):
|
|
|
19
19
|
self._columns = None
|
|
20
20
|
self._data_quality_warnings = None
|
|
21
21
|
|
|
22
|
+
def __str__(self):
|
|
23
|
+
return f"<{self.__class__.__name__} {self._id} {self._name} >"
|
|
24
|
+
|
|
25
|
+
def __repr__(self):
|
|
26
|
+
return self.__str__() + " { " + ", ".join(" % s: % s" % item for item in vars(self).items()) + "}"
|
|
27
|
+
|
|
22
28
|
@property
|
|
23
29
|
def permissions(self):
|
|
24
30
|
if self._permissions is None:
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
import abc
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Credentials(abc.ABC):
|
|
2
5
|
def __init__(self, site_id=None, user_id_to_impersonate=None):
|
|
3
6
|
self.site_id = site_id or ""
|
|
4
7
|
self.user_id_to_impersonate = user_id_to_impersonate or None
|
|
5
8
|
|
|
6
9
|
@property
|
|
10
|
+
@abc.abstractmethod
|
|
7
11
|
def credentials(self):
|
|
8
12
|
credentials = "Credentials can be username/password, Personal Access Token, or JWT"
|
|
9
13
|
+"This method returns values to set as an attribute on the credentials element of the request"
|
|
10
14
|
|
|
15
|
+
@abc.abstractmethod
|
|
11
16
|
def __repr__(self):
|
|
12
17
|
return "All Credentials types must have a debug display that does not print secrets"
|
|
13
18
|
|
|
@@ -38,7 +43,11 @@ class TableauAuth(Credentials):
|
|
|
38
43
|
return {"name": self.username, "password": self.password}
|
|
39
44
|
|
|
40
45
|
def __repr__(self):
|
|
41
|
-
|
|
46
|
+
if self.user_id_to_impersonate:
|
|
47
|
+
uid = f", user_id_to_impersonate=f{self.user_id_to_impersonate}"
|
|
48
|
+
else:
|
|
49
|
+
uid = ""
|
|
50
|
+
return f"<Credentials username={self.username} password=redacted (site={self.site_id}{uid})>"
|
|
42
51
|
|
|
43
52
|
@property
|
|
44
53
|
def site(self):
|
|
@@ -51,11 +60,12 @@ class TableauAuth(Credentials):
|
|
|
51
60
|
self.site_id = value
|
|
52
61
|
|
|
53
62
|
|
|
63
|
+
# A Tableau-generated Personal Access Token
|
|
54
64
|
class PersonalAccessTokenAuth(Credentials):
|
|
55
|
-
def __init__(self, token_name, personal_access_token, site_id=None):
|
|
65
|
+
def __init__(self, token_name, personal_access_token, site_id=None, user_id_to_impersonate=None):
|
|
56
66
|
if personal_access_token is None or token_name is None:
|
|
57
67
|
raise TabError("Must provide a token and token name when using PAT authentication")
|
|
58
|
-
super().__init__(site_id=site_id)
|
|
68
|
+
super().__init__(site_id=site_id, user_id_to_impersonate=user_id_to_impersonate)
|
|
59
69
|
self.token_name = token_name
|
|
60
70
|
self.personal_access_token = personal_access_token
|
|
61
71
|
|
|
@@ -67,6 +77,31 @@ class PersonalAccessTokenAuth(Credentials):
|
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
def __repr__(self):
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
if self.user_id_to_impersonate:
|
|
81
|
+
uid = f", user_id_to_impersonate=f{self.user_id_to_impersonate}"
|
|
82
|
+
else:
|
|
83
|
+
uid = ""
|
|
84
|
+
return (
|
|
85
|
+
f"<PersonalAccessToken name={self.token_name} token={self.personal_access_token[:2]}..."
|
|
86
|
+
f"(site={self.site_id}{uid} >"
|
|
72
87
|
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# A standard JWT generated specifically for Tableau
|
|
91
|
+
class JWTAuth(Credentials):
|
|
92
|
+
def __init__(self, jwt: str, site_id=None, user_id_to_impersonate=None):
|
|
93
|
+
if jwt is None:
|
|
94
|
+
raise TabError("Must provide a JWT token when using JWT authentication")
|
|
95
|
+
super().__init__(site_id, user_id_to_impersonate)
|
|
96
|
+
self.jwt = jwt
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def credentials(self):
|
|
100
|
+
return {"jwt": self.jwt}
|
|
101
|
+
|
|
102
|
+
def __repr__(self):
|
|
103
|
+
if self.user_id_to_impersonate:
|
|
104
|
+
uid = f", user_id_to_impersonate=f{self.user_id_to_impersonate}"
|
|
105
|
+
else:
|
|
106
|
+
uid = ""
|
|
107
|
+
return f"<{self.__class__.__qualname__} jwt={self.jwt[:5]}... (site={self.site_id}{uid})>"
|
|
@@ -5,23 +5,25 @@ from .flow_item import FlowItem
|
|
|
5
5
|
from .project_item import ProjectItem
|
|
6
6
|
from .view_item import ViewItem
|
|
7
7
|
from .workbook_item import WorkbookItem
|
|
8
|
+
from .metric_item import MetricItem
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class Resource:
|
|
11
12
|
Database = "database"
|
|
12
13
|
Datarole = "datarole"
|
|
14
|
+
Table = "table"
|
|
13
15
|
Datasource = "datasource"
|
|
14
16
|
Flow = "flow"
|
|
15
17
|
Lens = "lens"
|
|
16
18
|
Metric = "metric"
|
|
17
19
|
Project = "project"
|
|
18
|
-
Table = "table"
|
|
19
20
|
View = "view"
|
|
20
21
|
Workbook = "workbook"
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
# resource types that have permissions, can be renamed, etc
|
|
24
|
-
TableauItem
|
|
25
|
+
# todo: refactoring: should actually define TableauItem as an interface and let all these implement it
|
|
26
|
+
TableauItem = Union[DatasourceItem, FlowItem, MetricItem, ProjectItem, ViewItem, WorkbookItem]
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
def plural_type(content_type: Resource) -> str:
|
|
@@ -45,6 +45,7 @@ class UserItem(object):
|
|
|
45
45
|
class Auth:
|
|
46
46
|
OpenID = "OpenID"
|
|
47
47
|
SAML = "SAML"
|
|
48
|
+
TableauIDWithMFA = "TableauIDWithMFA"
|
|
48
49
|
ServerDefault = "ServerDefault"
|
|
49
50
|
|
|
50
51
|
def __init__(
|
|
@@ -66,10 +67,13 @@ class UserItem(object):
|
|
|
66
67
|
|
|
67
68
|
return None
|
|
68
69
|
|
|
69
|
-
def
|
|
70
|
+
def __str__(self) -> str:
|
|
70
71
|
str_site_role = self.site_role or "None"
|
|
71
72
|
return "<User {} name={} role={}>".format(self.id, self.name, str_site_role)
|
|
72
73
|
|
|
74
|
+
def __repr__(self):
|
|
75
|
+
return self.__str__() + " { " + ", ".join(" % s: % s" % item for item in vars(self).items()) + "}"
|
|
76
|
+
|
|
73
77
|
@property
|
|
74
78
|
def auth_setting(self) -> Optional[str]:
|
|
75
79
|
return self._auth_setting
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
from datetime import datetime
|
|
3
|
+
from requests import Response
|
|
3
4
|
from typing import Callable, Iterator, List, Optional, Set
|
|
4
5
|
|
|
5
6
|
from defusedxml.ElementTree import fromstring
|
|
@@ -31,11 +32,14 @@ class ViewItem(object):
|
|
|
31
32
|
self._permissions: Optional[Callable[[], List[PermissionsRule]]] = None
|
|
32
33
|
self.tags: Set[str] = set()
|
|
33
34
|
|
|
34
|
-
def
|
|
35
|
+
def __str__(self):
|
|
35
36
|
return "<ViewItem {0} '{1}' contentUrl='{2}' project={3}>".format(
|
|
36
37
|
self._id, self.name, self.content_url, self.project_id
|
|
37
38
|
)
|
|
38
39
|
|
|
40
|
+
def __repr__(self):
|
|
41
|
+
return self.__str__() + " { " + ", ".join(" % s: % s" % item for item in vars(self).items()) + "}"
|
|
42
|
+
|
|
39
43
|
def _set_preview_image(self, preview_image):
|
|
40
44
|
self._preview_image = preview_image
|
|
41
45
|
|
|
@@ -140,7 +144,7 @@ class ViewItem(object):
|
|
|
140
144
|
self._permissions = permissions
|
|
141
145
|
|
|
142
146
|
@classmethod
|
|
143
|
-
def from_response(cls, resp, ns, workbook_id="") -> List["ViewItem"]:
|
|
147
|
+
def from_response(cls, resp: "Response", ns, workbook_id="") -> List["ViewItem"]:
|
|
144
148
|
return cls.from_xml_element(fromstring(resp), ns, workbook_id)
|
|
145
149
|
|
|
146
150
|
@classmethod
|
|
@@ -148,39 +152,38 @@ class ViewItem(object):
|
|
|
148
152
|
all_view_items = list()
|
|
149
153
|
all_view_xml = parsed_response.findall(".//t:view", namespaces=ns)
|
|
150
154
|
for view_xml in all_view_xml:
|
|
151
|
-
view_item = cls()
|
|
152
|
-
usage_elem = view_xml.find(".//t:usage", namespaces=ns)
|
|
153
|
-
workbook_elem = view_xml.find(".//t:workbook", namespaces=ns)
|
|
154
|
-
owner_elem = view_xml.find(".//t:owner", namespaces=ns)
|
|
155
|
-
project_elem = view_xml.find(".//t:project", namespaces=ns)
|
|
156
|
-
tags_elem = view_xml.find(".//t:tags", namespaces=ns)
|
|
157
|
-
view_item._created_at = parse_datetime(view_xml.get("createdAt", None))
|
|
158
|
-
view_item._updated_at = parse_datetime(view_xml.get("updatedAt", None))
|
|
159
|
-
view_item._id = view_xml.get("id", None)
|
|
160
|
-
view_item._name = view_xml.get("name", None)
|
|
161
|
-
view_item._content_url = view_xml.get("contentUrl", None)
|
|
162
|
-
view_item._sheet_type = view_xml.get("sheetType", None)
|
|
163
|
-
|
|
164
|
-
if usage_elem is not None:
|
|
165
|
-
total_view = usage_elem.get("totalViewCount", None)
|
|
166
|
-
if total_view:
|
|
167
|
-
view_item._total_views = int(total_view)
|
|
168
|
-
|
|
169
|
-
if owner_elem is not None:
|
|
170
|
-
view_item._owner_id = owner_elem.get("id", None)
|
|
171
|
-
|
|
172
|
-
if project_elem is not None:
|
|
173
|
-
view_item._project_id = project_elem.get("id", None)
|
|
174
|
-
|
|
175
|
-
if workbook_id:
|
|
176
|
-
view_item._workbook_id = workbook_id
|
|
177
|
-
elif workbook_elem is not None:
|
|
178
|
-
view_item._workbook_id = workbook_elem.get("id", None)
|
|
179
|
-
|
|
180
|
-
if tags_elem is not None:
|
|
181
|
-
tags = TagItem.from_xml_element(tags_elem, ns)
|
|
182
|
-
view_item.tags = tags
|
|
183
|
-
view_item._initial_tags = copy.copy(tags)
|
|
184
|
-
|
|
155
|
+
view_item = cls.from_xml(view_xml, ns, workbook_id)
|
|
185
156
|
all_view_items.append(view_item)
|
|
186
157
|
return all_view_items
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def from_xml(cls, view_xml, ns, workbook_id="") -> "ViewItem":
|
|
161
|
+
view_item = cls()
|
|
162
|
+
usage_elem = view_xml.find(".//t:usage", namespaces=ns)
|
|
163
|
+
workbook_elem = view_xml.find(".//t:workbook", namespaces=ns)
|
|
164
|
+
owner_elem = view_xml.find(".//t:owner", namespaces=ns)
|
|
165
|
+
project_elem = view_xml.find(".//t:project", namespaces=ns)
|
|
166
|
+
tags_elem = view_xml.find(".//t:tags", namespaces=ns)
|
|
167
|
+
view_item._created_at = parse_datetime(view_xml.get("createdAt", None))
|
|
168
|
+
view_item._updated_at = parse_datetime(view_xml.get("updatedAt", None))
|
|
169
|
+
view_item._id = view_xml.get("id", None)
|
|
170
|
+
view_item._name = view_xml.get("name", None)
|
|
171
|
+
view_item._content_url = view_xml.get("contentUrl", None)
|
|
172
|
+
view_item._sheet_type = view_xml.get("sheetType", None)
|
|
173
|
+
if usage_elem is not None:
|
|
174
|
+
total_view = usage_elem.get("totalViewCount", None)
|
|
175
|
+
if total_view:
|
|
176
|
+
view_item._total_views = int(total_view)
|
|
177
|
+
if owner_elem is not None:
|
|
178
|
+
view_item._owner_id = owner_elem.get("id", None)
|
|
179
|
+
if project_elem is not None:
|
|
180
|
+
view_item._project_id = project_elem.get("id", None)
|
|
181
|
+
if workbook_id:
|
|
182
|
+
view_item._workbook_id = workbook_id
|
|
183
|
+
elif workbook_elem is not None:
|
|
184
|
+
view_item._workbook_id = workbook_elem.get("id", None)
|
|
185
|
+
if tags_elem is not None:
|
|
186
|
+
tags = TagItem.from_xml_element(tags_elem, ns)
|
|
187
|
+
view_item.tags = tags
|
|
188
|
+
view_item._initial_tags = copy.copy(tags)
|
|
189
|
+
return view_item
|