tableauserverclient 0.31__py3-none-any.whl → 0.33__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 +74 -4
- tableauserverclient/_version.py +3 -3
- tableauserverclient/config.py +16 -4
- tableauserverclient/models/__init__.py +100 -37
- tableauserverclient/models/connection_item.py +4 -2
- tableauserverclient/models/database_item.py +6 -0
- tableauserverclient/models/datasource_item.py +18 -6
- tableauserverclient/models/favorites_item.py +7 -7
- tableauserverclient/models/flow_item.py +6 -6
- tableauserverclient/models/groupset_item.py +53 -0
- tableauserverclient/models/interval_item.py +27 -14
- tableauserverclient/models/job_item.py +18 -2
- tableauserverclient/models/linked_tasks_item.py +102 -0
- tableauserverclient/models/permissions_item.py +53 -5
- tableauserverclient/models/project_item.py +4 -3
- tableauserverclient/models/reference_item.py +5 -0
- tableauserverclient/models/tableau_auth.py +22 -23
- tableauserverclient/models/tableau_types.py +9 -7
- tableauserverclient/models/virtual_connection_item.py +77 -0
- tableauserverclient/server/__init__.py +83 -8
- tableauserverclient/server/endpoint/__init__.py +69 -28
- tableauserverclient/server/endpoint/auth_endpoint.py +3 -3
- tableauserverclient/server/endpoint/custom_views_endpoint.py +66 -5
- tableauserverclient/server/endpoint/databases_endpoint.py +21 -18
- tableauserverclient/server/endpoint/datasources_endpoint.py +115 -35
- tableauserverclient/server/endpoint/endpoint.py +53 -20
- tableauserverclient/server/endpoint/favorites_endpoint.py +1 -1
- tableauserverclient/server/endpoint/fileuploads_endpoint.py +2 -2
- tableauserverclient/server/endpoint/flow_runs_endpoint.py +45 -5
- tableauserverclient/server/endpoint/flows_endpoint.py +43 -16
- tableauserverclient/server/endpoint/groups_endpoint.py +85 -31
- tableauserverclient/server/endpoint/groupsets_endpoint.py +127 -0
- tableauserverclient/server/endpoint/jobs_endpoint.py +74 -7
- tableauserverclient/server/endpoint/linked_tasks_endpoint.py +45 -0
- tableauserverclient/server/endpoint/metadata_endpoint.py +3 -3
- tableauserverclient/server/endpoint/metrics_endpoint.py +1 -1
- tableauserverclient/server/endpoint/projects_endpoint.py +50 -18
- tableauserverclient/server/endpoint/resource_tagger.py +135 -3
- tableauserverclient/server/endpoint/tables_endpoint.py +19 -16
- tableauserverclient/server/endpoint/users_endpoint.py +40 -1
- tableauserverclient/server/endpoint/views_endpoint.py +97 -10
- tableauserverclient/server/endpoint/virtual_connections_endpoint.py +173 -0
- tableauserverclient/server/endpoint/workbooks_endpoint.py +97 -45
- tableauserverclient/server/pager.py +46 -46
- tableauserverclient/server/query.py +62 -33
- tableauserverclient/server/request_factory.py +192 -49
- tableauserverclient/server/request_options.py +4 -2
- tableauserverclient/server/server.py +13 -6
- {tableauserverclient-0.31.dist-info → tableauserverclient-0.33.dist-info}/METADATA +15 -16
- {tableauserverclient-0.31.dist-info → tableauserverclient-0.33.dist-info}/RECORD +54 -48
- {tableauserverclient-0.31.dist-info → tableauserverclient-0.33.dist-info}/WHEEL +1 -1
- {tableauserverclient-0.31.dist-info → tableauserverclient-0.33.dist-info}/LICENSE +0 -0
- {tableauserverclient-0.31.dist-info → tableauserverclient-0.33.dist-info}/LICENSE.versioneer +0 -0
- {tableauserverclient-0.31.dist-info → tableauserverclient-0.33.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from defusedxml.ElementTree import fromstring
|
|
5
|
+
|
|
6
|
+
from tableauserverclient.datetime_helpers import parse_datetime
|
|
7
|
+
from tableauserverclient.models.schedule_item import ScheduleItem
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LinkedTaskItem:
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self.id: Optional[str] = None
|
|
13
|
+
self.num_steps: Optional[int] = None
|
|
14
|
+
self.schedule: Optional[ScheduleItem] = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_response(cls, resp: bytes, namespace) -> List["LinkedTaskItem"]:
|
|
18
|
+
parsed_response = fromstring(resp)
|
|
19
|
+
return [
|
|
20
|
+
cls._parse_element(x, namespace)
|
|
21
|
+
for x in parsed_response.findall(".//t:linkedTasks[@id]", namespaces=namespace)
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def _parse_element(cls, xml, namespace) -> "LinkedTaskItem":
|
|
26
|
+
task = cls()
|
|
27
|
+
task.id = xml.get("id")
|
|
28
|
+
task.num_steps = int(xml.get("numSteps"))
|
|
29
|
+
task.schedule = ScheduleItem.from_element(xml, namespace)[0]
|
|
30
|
+
return task
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class LinkedTaskStepItem:
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
self.id: Optional[str] = None
|
|
36
|
+
self.step_number: Optional[int] = None
|
|
37
|
+
self.stop_downstream_on_failure: Optional[bool] = None
|
|
38
|
+
self.task_details: List[LinkedTaskFlowRunItem] = []
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_task_xml(cls, xml, namespace) -> List["LinkedTaskStepItem"]:
|
|
42
|
+
return [cls._parse_element(x, namespace) for x in xml.findall(".//t:linkedTaskSteps[@id]", namespace)]
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def _parse_element(cls, xml, namespace) -> "LinkedTaskStepItem":
|
|
46
|
+
step = cls()
|
|
47
|
+
step.id = xml.get("id")
|
|
48
|
+
step.step_number = int(xml.get("stepNumber"))
|
|
49
|
+
step.stop_downstream_on_failure = string_to_bool(xml.get("stopDownstreamTasksOnFailure"))
|
|
50
|
+
step.task_details = LinkedTaskFlowRunItem._parse_element(xml, namespace)
|
|
51
|
+
return step
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class LinkedTaskFlowRunItem:
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
self.flow_run_id: Optional[str] = None
|
|
57
|
+
self.flow_run_priority: Optional[int] = None
|
|
58
|
+
self.flow_run_consecutive_failed_count: Optional[int] = None
|
|
59
|
+
self.flow_run_task_type: Optional[str] = None
|
|
60
|
+
self.flow_id: Optional[str] = None
|
|
61
|
+
self.flow_name: Optional[str] = None
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def _parse_element(cls, xml, namespace) -> List["LinkedTaskFlowRunItem"]:
|
|
65
|
+
all_tasks = []
|
|
66
|
+
for flow_run in xml.findall(".//t:flowRun[@id]", namespace):
|
|
67
|
+
task = cls()
|
|
68
|
+
task.flow_run_id = flow_run.get("id")
|
|
69
|
+
task.flow_run_priority = int(flow_run.get("priority"))
|
|
70
|
+
task.flow_run_consecutive_failed_count = int(flow_run.get("consecutiveFailedCount"))
|
|
71
|
+
task.flow_run_task_type = flow_run.get("type")
|
|
72
|
+
flow = flow_run.find(".//t:flow[@id]", namespace)
|
|
73
|
+
task.flow_id = flow.get("id")
|
|
74
|
+
task.flow_name = flow.get("name")
|
|
75
|
+
all_tasks.append(task)
|
|
76
|
+
|
|
77
|
+
return all_tasks
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class LinkedTaskJobItem:
|
|
81
|
+
def __init__(self) -> None:
|
|
82
|
+
self.id: Optional[str] = None
|
|
83
|
+
self.linked_task_id: Optional[str] = None
|
|
84
|
+
self.status: Optional[str] = None
|
|
85
|
+
self.created_at: Optional[dt.datetime] = None
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_response(cls, resp: bytes, namespace) -> "LinkedTaskJobItem":
|
|
89
|
+
parsed_response = fromstring(resp)
|
|
90
|
+
job = cls()
|
|
91
|
+
job_xml = parsed_response.find(".//t:linkedTaskJob[@id]", namespaces=namespace)
|
|
92
|
+
if job_xml is None:
|
|
93
|
+
raise ValueError("No linked task job found in response")
|
|
94
|
+
job.id = job_xml.get("id")
|
|
95
|
+
job.linked_task_id = job_xml.get("linkedTaskId")
|
|
96
|
+
job.status = job_xml.get("status")
|
|
97
|
+
job.created_at = parse_datetime(job_xml.get("createdAt"))
|
|
98
|
+
return job
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def string_to_bool(s: str) -> bool:
|
|
102
|
+
return s.lower() == "true"
|
|
@@ -3,10 +3,11 @@ from typing import Dict, List, Optional
|
|
|
3
3
|
|
|
4
4
|
from defusedxml.ElementTree import fromstring
|
|
5
5
|
|
|
6
|
-
from .exceptions import UnknownGranteeTypeError, UnpopulatedPropertyError
|
|
7
|
-
from .group_item import GroupItem
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
6
|
+
from tableauserverclient.models.exceptions import UnknownGranteeTypeError, UnpopulatedPropertyError
|
|
7
|
+
from tableauserverclient.models.group_item import GroupItem
|
|
8
|
+
from tableauserverclient.models.groupset_item import GroupSetItem
|
|
9
|
+
from tableauserverclient.models.reference_item import ResourceReference
|
|
10
|
+
from tableauserverclient.models.user_item import UserItem
|
|
10
11
|
|
|
11
12
|
from tableauserverclient.helpers.logging import logger
|
|
12
13
|
|
|
@@ -45,7 +46,7 @@ class Permission:
|
|
|
45
46
|
return "<Enum Capability: AddComment | ChangeHierarchy | ChangePermission ... (17 more) >"
|
|
46
47
|
|
|
47
48
|
|
|
48
|
-
class PermissionsRule
|
|
49
|
+
class PermissionsRule:
|
|
49
50
|
def __init__(self, grantee: ResourceReference, capabilities: Dict[str, str]) -> None:
|
|
50
51
|
self.grantee = grantee
|
|
51
52
|
self.capabilities = capabilities
|
|
@@ -53,6 +54,51 @@ class PermissionsRule(object):
|
|
|
53
54
|
def __repr__(self):
|
|
54
55
|
return "<PermissionsRule grantee={}, capabilities={}>".format(self.grantee, self.capabilities)
|
|
55
56
|
|
|
57
|
+
def __eq__(self, other: object) -> bool:
|
|
58
|
+
if not hasattr(other, "grantee") or not hasattr(other, "capabilities"):
|
|
59
|
+
return False
|
|
60
|
+
return self.grantee == other.grantee and self.capabilities == other.capabilities
|
|
61
|
+
|
|
62
|
+
def __and__(self, other: "PermissionsRule") -> "PermissionsRule":
|
|
63
|
+
if self.grantee != other.grantee:
|
|
64
|
+
raise ValueError("Cannot AND two permissions rules with different grantees")
|
|
65
|
+
|
|
66
|
+
if self.capabilities == other.capabilities:
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
capabilities = set((*self.capabilities.keys(), *other.capabilities.keys()))
|
|
70
|
+
new_capabilities = {}
|
|
71
|
+
for capability in capabilities:
|
|
72
|
+
if (self.capabilities.get(capability), other.capabilities.get(capability)) == (
|
|
73
|
+
Permission.Mode.Allow,
|
|
74
|
+
Permission.Mode.Allow,
|
|
75
|
+
):
|
|
76
|
+
new_capabilities[capability] = Permission.Mode.Allow
|
|
77
|
+
elif Permission.Mode.Deny in (self.capabilities.get(capability), other.capabilities.get(capability)):
|
|
78
|
+
new_capabilities[capability] = Permission.Mode.Deny
|
|
79
|
+
|
|
80
|
+
return PermissionsRule(self.grantee, new_capabilities)
|
|
81
|
+
|
|
82
|
+
def __or__(self, other: "PermissionsRule") -> "PermissionsRule":
|
|
83
|
+
if self.grantee != other.grantee:
|
|
84
|
+
raise ValueError("Cannot OR two permissions rules with different grantees")
|
|
85
|
+
|
|
86
|
+
if self.capabilities == other.capabilities:
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
capabilities = set((*self.capabilities.keys(), *other.capabilities.keys()))
|
|
90
|
+
new_capabilities = {}
|
|
91
|
+
for capability in capabilities:
|
|
92
|
+
if Permission.Mode.Allow in (self.capabilities.get(capability), other.capabilities.get(capability)):
|
|
93
|
+
new_capabilities[capability] = Permission.Mode.Allow
|
|
94
|
+
elif (self.capabilities.get(capability), other.capabilities.get(capability)) == (
|
|
95
|
+
Permission.Mode.Deny,
|
|
96
|
+
Permission.Mode.Deny,
|
|
97
|
+
):
|
|
98
|
+
new_capabilities[capability] = Permission.Mode.Deny
|
|
99
|
+
|
|
100
|
+
return PermissionsRule(self.grantee, new_capabilities)
|
|
101
|
+
|
|
56
102
|
@classmethod
|
|
57
103
|
def from_response(cls, resp, ns=None) -> List["PermissionsRule"]:
|
|
58
104
|
parsed_response = fromstring(resp)
|
|
@@ -97,6 +143,8 @@ class PermissionsRule(object):
|
|
|
97
143
|
grantee = UserItem.as_reference(grantee_id)
|
|
98
144
|
elif grantee_type == "group":
|
|
99
145
|
grantee = GroupItem.as_reference(grantee_id)
|
|
146
|
+
elif grantee_type == "groupSet":
|
|
147
|
+
grantee = GroupSetItem.as_reference(grantee_id)
|
|
100
148
|
else:
|
|
101
149
|
raise UnknownGranteeTypeError("No support for grantee type of {}".format(grantee_type))
|
|
102
150
|
|
|
@@ -4,8 +4,8 @@ from typing import List, Optional
|
|
|
4
4
|
|
|
5
5
|
from defusedxml.ElementTree import fromstring
|
|
6
6
|
|
|
7
|
-
from .exceptions import UnpopulatedPropertyError
|
|
8
|
-
from .property_decorators import property_is_enum, property_not_empty
|
|
7
|
+
from tableauserverclient.models.exceptions import UnpopulatedPropertyError
|
|
8
|
+
from tableauserverclient.models.property_decorators import property_is_enum, property_not_empty
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class ProjectItem(object):
|
|
@@ -34,6 +34,7 @@ class ProjectItem(object):
|
|
|
34
34
|
self.content_permissions: Optional[str] = content_permissions
|
|
35
35
|
self.parent_id: Optional[str] = parent_id
|
|
36
36
|
self._samples: Optional[bool] = samples
|
|
37
|
+
self._owner_id: Optional[str] = None
|
|
37
38
|
|
|
38
39
|
self._permissions = None
|
|
39
40
|
self._default_workbook_permissions = None
|
|
@@ -119,7 +120,7 @@ class ProjectItem(object):
|
|
|
119
120
|
|
|
120
121
|
@owner_id.setter
|
|
121
122
|
def owner_id(self, value: str) -> None:
|
|
122
|
-
|
|
123
|
+
self._owner_id = value
|
|
123
124
|
|
|
124
125
|
def is_default(self):
|
|
125
126
|
return self.name.lower() == "default"
|
|
@@ -8,6 +8,11 @@ class ResourceReference(object):
|
|
|
8
8
|
|
|
9
9
|
__repr__ = __str__
|
|
10
10
|
|
|
11
|
+
def __eq__(self, other: object) -> bool:
|
|
12
|
+
if not hasattr(other, "id") or not hasattr(other, "tag_name"):
|
|
13
|
+
return False
|
|
14
|
+
return (self.id == other.id) and (self.tag_name == other.tag_name)
|
|
15
|
+
|
|
11
16
|
@property
|
|
12
17
|
def id(self):
|
|
13
18
|
return self._id
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import abc
|
|
2
|
+
from typing import Dict, Optional
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class Credentials(abc.ABC):
|
|
5
|
-
def __init__(self, site_id=None, user_id_to_impersonate=None):
|
|
6
|
+
def __init__(self, site_id: Optional[str] = None, user_id_to_impersonate: Optional[str] = None) -> None:
|
|
6
7
|
self.site_id = site_id or ""
|
|
7
8
|
self.user_id_to_impersonate = user_id_to_impersonate or None
|
|
8
9
|
|
|
9
10
|
@property
|
|
10
11
|
@abc.abstractmethod
|
|
11
|
-
def credentials(self):
|
|
12
|
-
credentials =
|
|
13
|
-
|
|
12
|
+
def credentials(self) -> Dict[str, str]:
|
|
13
|
+
credentials = (
|
|
14
|
+
"Credentials can be username/password, Personal Access Token, or JWT"
|
|
15
|
+
"This method returns values to set as an attribute on the credentials element of the request"
|
|
16
|
+
)
|
|
17
|
+
return {"key": "value"}
|
|
14
18
|
|
|
15
19
|
@abc.abstractmethod
|
|
16
20
|
def __repr__(self):
|
|
@@ -28,10 +32,9 @@ def deprecate_site_attribute():
|
|
|
28
32
|
|
|
29
33
|
# The traditional auth type: username/password
|
|
30
34
|
class TableauAuth(Credentials):
|
|
31
|
-
def __init__(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
site_id = site
|
|
35
|
+
def __init__(
|
|
36
|
+
self, username: str, password: str, site_id: Optional[str] = None, user_id_to_impersonate: Optional[str] = None
|
|
37
|
+
) -> None:
|
|
35
38
|
super().__init__(site_id, user_id_to_impersonate)
|
|
36
39
|
if password is None:
|
|
37
40
|
raise TabError("Must provide a password when using traditional authentication")
|
|
@@ -39,7 +42,7 @@ class TableauAuth(Credentials):
|
|
|
39
42
|
self.username = username
|
|
40
43
|
|
|
41
44
|
@property
|
|
42
|
-
def credentials(self):
|
|
45
|
+
def credentials(self) -> Dict[str, str]:
|
|
43
46
|
return {"name": self.username, "password": self.password}
|
|
44
47
|
|
|
45
48
|
def __repr__(self):
|
|
@@ -49,20 +52,16 @@ class TableauAuth(Credentials):
|
|
|
49
52
|
uid = ""
|
|
50
53
|
return f"<Credentials username={self.username} password=redacted (site={self.site_id}{uid})>"
|
|
51
54
|
|
|
52
|
-
@property
|
|
53
|
-
def site(self):
|
|
54
|
-
deprecate_site_attribute()
|
|
55
|
-
return self.site_id
|
|
56
|
-
|
|
57
|
-
@site.setter
|
|
58
|
-
def site(self, value):
|
|
59
|
-
deprecate_site_attribute()
|
|
60
|
-
self.site_id = value
|
|
61
|
-
|
|
62
55
|
|
|
63
56
|
# A Tableau-generated Personal Access Token
|
|
64
57
|
class PersonalAccessTokenAuth(Credentials):
|
|
65
|
-
def __init__(
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
token_name: str,
|
|
61
|
+
personal_access_token: str,
|
|
62
|
+
site_id: Optional[str] = None,
|
|
63
|
+
user_id_to_impersonate: Optional[str] = None,
|
|
64
|
+
) -> None:
|
|
66
65
|
if personal_access_token is None or token_name is None:
|
|
67
66
|
raise TabError("Must provide a token and token name when using PAT authentication")
|
|
68
67
|
super().__init__(site_id=site_id, user_id_to_impersonate=user_id_to_impersonate)
|
|
@@ -70,7 +69,7 @@ class PersonalAccessTokenAuth(Credentials):
|
|
|
70
69
|
self.personal_access_token = personal_access_token
|
|
71
70
|
|
|
72
71
|
@property
|
|
73
|
-
def credentials(self):
|
|
72
|
+
def credentials(self) -> Dict[str, str]:
|
|
74
73
|
return {
|
|
75
74
|
"personalAccessTokenName": self.token_name,
|
|
76
75
|
"personalAccessTokenSecret": self.personal_access_token,
|
|
@@ -89,14 +88,14 @@ class PersonalAccessTokenAuth(Credentials):
|
|
|
89
88
|
|
|
90
89
|
# A standard JWT generated specifically for Tableau
|
|
91
90
|
class JWTAuth(Credentials):
|
|
92
|
-
def __init__(self, jwt: str, site_id=None, user_id_to_impersonate=None):
|
|
91
|
+
def __init__(self, jwt: str, site_id: Optional[str] = None, user_id_to_impersonate: Optional[str] = None) -> None:
|
|
93
92
|
if jwt is None:
|
|
94
93
|
raise TabError("Must provide a JWT token when using JWT authentication")
|
|
95
94
|
super().__init__(site_id, user_id_to_impersonate)
|
|
96
95
|
self.jwt = jwt
|
|
97
96
|
|
|
98
97
|
@property
|
|
99
|
-
def credentials(self):
|
|
98
|
+
def credentials(self) -> Dict[str, str]:
|
|
100
99
|
return {"jwt": self.jwt}
|
|
101
100
|
|
|
102
101
|
def __repr__(self):
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from typing import Union
|
|
2
2
|
|
|
3
|
-
from .datasource_item import DatasourceItem
|
|
4
|
-
from .flow_item import FlowItem
|
|
5
|
-
from .project_item import ProjectItem
|
|
6
|
-
from .view_item import ViewItem
|
|
7
|
-
from .workbook_item import WorkbookItem
|
|
8
|
-
from .metric_item import MetricItem
|
|
3
|
+
from tableauserverclient.models.datasource_item import DatasourceItem
|
|
4
|
+
from tableauserverclient.models.flow_item import FlowItem
|
|
5
|
+
from tableauserverclient.models.project_item import ProjectItem
|
|
6
|
+
from tableauserverclient.models.view_item import ViewItem
|
|
7
|
+
from tableauserverclient.models.workbook_item import WorkbookItem
|
|
8
|
+
from tableauserverclient.models.metric_item import MetricItem
|
|
9
|
+
from tableauserverclient.models.virtual_connection_item import VirtualConnectionItem
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class Resource:
|
|
@@ -18,12 +19,13 @@ class Resource:
|
|
|
18
19
|
Metric = "metric"
|
|
19
20
|
Project = "project"
|
|
20
21
|
View = "view"
|
|
22
|
+
VirtualConnection = "virtualConnection"
|
|
21
23
|
Workbook = "workbook"
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
# resource types that have permissions, can be renamed, etc
|
|
25
27
|
# todo: refactoring: should actually define TableauItem as an interface and let all these implement it
|
|
26
|
-
TableauItem = Union[DatasourceItem, FlowItem, MetricItem, ProjectItem, ViewItem, WorkbookItem]
|
|
28
|
+
TableauItem = Union[DatasourceItem, FlowItem, MetricItem, ProjectItem, ViewItem, WorkbookItem, VirtualConnectionItem]
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
def plural_type(content_type: Resource) -> str:
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
import json
|
|
3
|
+
from typing import Callable, Dict, Iterable, List, Optional
|
|
4
|
+
from xml.etree.ElementTree import Element
|
|
5
|
+
|
|
6
|
+
from defusedxml.ElementTree import fromstring
|
|
7
|
+
|
|
8
|
+
from tableauserverclient.datetime_helpers import parse_datetime
|
|
9
|
+
from tableauserverclient.models.connection_item import ConnectionItem
|
|
10
|
+
from tableauserverclient.models.exceptions import UnpopulatedPropertyError
|
|
11
|
+
from tableauserverclient.models.permissions_item import PermissionsRule
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class VirtualConnectionItem:
|
|
15
|
+
def __init__(self, name: str) -> None:
|
|
16
|
+
self.name = name
|
|
17
|
+
self.created_at: Optional[dt.datetime] = None
|
|
18
|
+
self.has_extracts: Optional[bool] = None
|
|
19
|
+
self._id: Optional[str] = None
|
|
20
|
+
self.is_certified: Optional[bool] = None
|
|
21
|
+
self.updated_at: Optional[dt.datetime] = None
|
|
22
|
+
self.webpage_url: Optional[str] = None
|
|
23
|
+
self._connections: Optional[Callable[[], Iterable[ConnectionItem]]] = None
|
|
24
|
+
self.project_id: Optional[str] = None
|
|
25
|
+
self.owner_id: Optional[str] = None
|
|
26
|
+
self.content: Optional[Dict[str, dict]] = None
|
|
27
|
+
self.certification_note: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
return f"{self.__class__.__qualname__}(name={self.name})"
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str:
|
|
33
|
+
return f"<{self!s}>"
|
|
34
|
+
|
|
35
|
+
def _set_permissions(self, permissions):
|
|
36
|
+
self._permissions = permissions
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def id(self) -> Optional[str]:
|
|
40
|
+
return self._id
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def permissions(self) -> List[PermissionsRule]:
|
|
44
|
+
if self._permissions is None:
|
|
45
|
+
error = "Workbook item must be populated with permissions first."
|
|
46
|
+
raise UnpopulatedPropertyError(error)
|
|
47
|
+
return self._permissions()
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def connections(self) -> Iterable[ConnectionItem]:
|
|
51
|
+
if self._connections is None:
|
|
52
|
+
raise AttributeError("connections not populated. Call populate_connections() first.")
|
|
53
|
+
return self._connections()
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_response(cls, response: bytes, ns: Dict[str, str]) -> List["VirtualConnectionItem"]:
|
|
57
|
+
parsed_response = fromstring(response)
|
|
58
|
+
return [cls.from_xml(xml, ns) for xml in parsed_response.findall(".//t:virtualConnection[@name]", ns)]
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_xml(cls, xml: Element, ns: Dict[str, str]) -> "VirtualConnectionItem":
|
|
62
|
+
v_conn = cls(xml.get("name", ""))
|
|
63
|
+
v_conn._id = xml.get("id", None)
|
|
64
|
+
v_conn.webpage_url = xml.get("webpageUrl", None)
|
|
65
|
+
v_conn.created_at = parse_datetime(xml.get("createdAt", None))
|
|
66
|
+
v_conn.updated_at = parse_datetime(xml.get("updatedAt", None))
|
|
67
|
+
v_conn.is_certified = string_to_bool(s) if (s := xml.get("isCertified", None)) else None
|
|
68
|
+
v_conn.certification_note = xml.get("certificationNote", None)
|
|
69
|
+
v_conn.has_extracts = string_to_bool(s) if (s := xml.get("hasExtracts", None)) else None
|
|
70
|
+
v_conn.project_id = p.get("id", None) if ((p := xml.find(".//t:project[@id]", ns)) is not None) else None
|
|
71
|
+
v_conn.owner_id = o.get("id", None) if ((o := xml.find(".//t:owner[@id]", ns)) is not None) else None
|
|
72
|
+
v_conn.content = json.loads(c.text or "{}") if ((c := xml.find(".//t:content", ns)) is not None) else None
|
|
73
|
+
return v_conn
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def string_to_bool(s: str) -> bool:
|
|
77
|
+
return s.lower() in ["true", "1", "t", "y", "yes"]
|
|
@@ -1,16 +1,91 @@
|
|
|
1
1
|
# These two imports must come first
|
|
2
|
-
from .request_factory import RequestFactory
|
|
3
|
-
from .request_options import (
|
|
2
|
+
from tableauserverclient.server.request_factory import RequestFactory
|
|
3
|
+
from tableauserverclient.server.request_options import (
|
|
4
4
|
CSVRequestOptions,
|
|
5
5
|
ExcelRequestOptions,
|
|
6
6
|
ImageRequestOptions,
|
|
7
7
|
PDFRequestOptions,
|
|
8
8
|
RequestOptions,
|
|
9
9
|
)
|
|
10
|
+
from tableauserverclient.server.filter import Filter
|
|
11
|
+
from tableauserverclient.server.sort import Sort
|
|
12
|
+
from tableauserverclient.server.server import Server
|
|
13
|
+
from tableauserverclient.server.pager import Pager
|
|
14
|
+
from tableauserverclient.server.endpoint.exceptions import NotSignedInError
|
|
10
15
|
|
|
11
|
-
from .
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
from tableauserverclient.server.endpoint import (
|
|
17
|
+
Auth,
|
|
18
|
+
CustomViews,
|
|
19
|
+
DataAccelerationReport,
|
|
20
|
+
DataAlerts,
|
|
21
|
+
Databases,
|
|
22
|
+
Datasources,
|
|
23
|
+
QuerysetEndpoint,
|
|
24
|
+
MissingRequiredFieldError,
|
|
25
|
+
Endpoint,
|
|
26
|
+
Favorites,
|
|
27
|
+
Fileuploads,
|
|
28
|
+
FlowRuns,
|
|
29
|
+
Flows,
|
|
30
|
+
FlowTasks,
|
|
31
|
+
Groups,
|
|
32
|
+
Jobs,
|
|
33
|
+
Metadata,
|
|
34
|
+
Metrics,
|
|
35
|
+
Projects,
|
|
36
|
+
Schedules,
|
|
37
|
+
ServerInfo,
|
|
38
|
+
ServerResponseError,
|
|
39
|
+
Sites,
|
|
40
|
+
Subscriptions,
|
|
41
|
+
Tables,
|
|
42
|
+
Tasks,
|
|
43
|
+
Users,
|
|
44
|
+
Views,
|
|
45
|
+
Webhooks,
|
|
46
|
+
Workbooks,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"RequestFactory",
|
|
51
|
+
"CSVRequestOptions",
|
|
52
|
+
"ExcelRequestOptions",
|
|
53
|
+
"ImageRequestOptions",
|
|
54
|
+
"PDFRequestOptions",
|
|
55
|
+
"RequestOptions",
|
|
56
|
+
"Filter",
|
|
57
|
+
"Sort",
|
|
58
|
+
"Server",
|
|
59
|
+
"Pager",
|
|
60
|
+
"NotSignedInError",
|
|
61
|
+
"Auth",
|
|
62
|
+
"CustomViews",
|
|
63
|
+
"DataAccelerationReport",
|
|
64
|
+
"DataAlerts",
|
|
65
|
+
"Databases",
|
|
66
|
+
"Datasources",
|
|
67
|
+
"QuerysetEndpoint",
|
|
68
|
+
"MissingRequiredFieldError",
|
|
69
|
+
"Endpoint",
|
|
70
|
+
"Favorites",
|
|
71
|
+
"Fileuploads",
|
|
72
|
+
"FlowRuns",
|
|
73
|
+
"Flows",
|
|
74
|
+
"FlowTasks",
|
|
75
|
+
"Groups",
|
|
76
|
+
"Jobs",
|
|
77
|
+
"Metadata",
|
|
78
|
+
"Metrics",
|
|
79
|
+
"Projects",
|
|
80
|
+
"Schedules",
|
|
81
|
+
"ServerInfo",
|
|
82
|
+
"ServerResponseError",
|
|
83
|
+
"Sites",
|
|
84
|
+
"Subscriptions",
|
|
85
|
+
"Tables",
|
|
86
|
+
"Tasks",
|
|
87
|
+
"Users",
|
|
88
|
+
"Views",
|
|
89
|
+
"Webhooks",
|
|
90
|
+
"Workbooks",
|
|
91
|
+
]
|
|
@@ -1,28 +1,69 @@
|
|
|
1
|
-
from .auth_endpoint import Auth
|
|
2
|
-
from .custom_views_endpoint import CustomViews
|
|
3
|
-
from .data_acceleration_report_endpoint import DataAccelerationReport
|
|
4
|
-
from .data_alert_endpoint import DataAlerts
|
|
5
|
-
from .databases_endpoint import Databases
|
|
6
|
-
from .datasources_endpoint import Datasources
|
|
7
|
-
from .endpoint import Endpoint, QuerysetEndpoint
|
|
8
|
-
from .exceptions import ServerResponseError, MissingRequiredFieldError
|
|
9
|
-
from .favorites_endpoint import Favorites
|
|
10
|
-
from .fileuploads_endpoint import Fileuploads
|
|
11
|
-
from .flow_runs_endpoint import FlowRuns
|
|
12
|
-
from .flows_endpoint import Flows
|
|
13
|
-
from .flow_task_endpoint import FlowTasks
|
|
14
|
-
from .groups_endpoint import Groups
|
|
15
|
-
from .
|
|
16
|
-
from .
|
|
17
|
-
from .
|
|
18
|
-
from .
|
|
19
|
-
from .
|
|
20
|
-
from .
|
|
21
|
-
from .
|
|
22
|
-
from .
|
|
23
|
-
from .
|
|
24
|
-
from .
|
|
25
|
-
from .
|
|
26
|
-
from .
|
|
27
|
-
from .
|
|
28
|
-
from .
|
|
1
|
+
from tableauserverclient.server.endpoint.auth_endpoint import Auth
|
|
2
|
+
from tableauserverclient.server.endpoint.custom_views_endpoint import CustomViews
|
|
3
|
+
from tableauserverclient.server.endpoint.data_acceleration_report_endpoint import DataAccelerationReport
|
|
4
|
+
from tableauserverclient.server.endpoint.data_alert_endpoint import DataAlerts
|
|
5
|
+
from tableauserverclient.server.endpoint.databases_endpoint import Databases
|
|
6
|
+
from tableauserverclient.server.endpoint.datasources_endpoint import Datasources
|
|
7
|
+
from tableauserverclient.server.endpoint.endpoint import Endpoint, QuerysetEndpoint
|
|
8
|
+
from tableauserverclient.server.endpoint.exceptions import ServerResponseError, MissingRequiredFieldError
|
|
9
|
+
from tableauserverclient.server.endpoint.favorites_endpoint import Favorites
|
|
10
|
+
from tableauserverclient.server.endpoint.fileuploads_endpoint import Fileuploads
|
|
11
|
+
from tableauserverclient.server.endpoint.flow_runs_endpoint import FlowRuns
|
|
12
|
+
from tableauserverclient.server.endpoint.flows_endpoint import Flows
|
|
13
|
+
from tableauserverclient.server.endpoint.flow_task_endpoint import FlowTasks
|
|
14
|
+
from tableauserverclient.server.endpoint.groups_endpoint import Groups
|
|
15
|
+
from tableauserverclient.server.endpoint.groupsets_endpoint import GroupSets
|
|
16
|
+
from tableauserverclient.server.endpoint.jobs_endpoint import Jobs
|
|
17
|
+
from tableauserverclient.server.endpoint.linked_tasks_endpoint import LinkedTasks
|
|
18
|
+
from tableauserverclient.server.endpoint.metadata_endpoint import Metadata
|
|
19
|
+
from tableauserverclient.server.endpoint.metrics_endpoint import Metrics
|
|
20
|
+
from tableauserverclient.server.endpoint.projects_endpoint import Projects
|
|
21
|
+
from tableauserverclient.server.endpoint.schedules_endpoint import Schedules
|
|
22
|
+
from tableauserverclient.server.endpoint.server_info_endpoint import ServerInfo
|
|
23
|
+
from tableauserverclient.server.endpoint.sites_endpoint import Sites
|
|
24
|
+
from tableauserverclient.server.endpoint.subscriptions_endpoint import Subscriptions
|
|
25
|
+
from tableauserverclient.server.endpoint.tables_endpoint import Tables
|
|
26
|
+
from tableauserverclient.server.endpoint.resource_tagger import Tags
|
|
27
|
+
from tableauserverclient.server.endpoint.tasks_endpoint import Tasks
|
|
28
|
+
from tableauserverclient.server.endpoint.users_endpoint import Users
|
|
29
|
+
from tableauserverclient.server.endpoint.views_endpoint import Views
|
|
30
|
+
from tableauserverclient.server.endpoint.virtual_connections_endpoint import VirtualConnections
|
|
31
|
+
from tableauserverclient.server.endpoint.webhooks_endpoint import Webhooks
|
|
32
|
+
from tableauserverclient.server.endpoint.workbooks_endpoint import Workbooks
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"Auth",
|
|
36
|
+
"CustomViews",
|
|
37
|
+
"DataAccelerationReport",
|
|
38
|
+
"DataAlerts",
|
|
39
|
+
"Databases",
|
|
40
|
+
"Datasources",
|
|
41
|
+
"QuerysetEndpoint",
|
|
42
|
+
"MissingRequiredFieldError",
|
|
43
|
+
"Endpoint",
|
|
44
|
+
"Favorites",
|
|
45
|
+
"Fileuploads",
|
|
46
|
+
"FlowRuns",
|
|
47
|
+
"Flows",
|
|
48
|
+
"FlowTasks",
|
|
49
|
+
"Groups",
|
|
50
|
+
"GroupSets",
|
|
51
|
+
"Jobs",
|
|
52
|
+
"LinkedTasks",
|
|
53
|
+
"Metadata",
|
|
54
|
+
"Metrics",
|
|
55
|
+
"Projects",
|
|
56
|
+
"Schedules",
|
|
57
|
+
"ServerInfo",
|
|
58
|
+
"ServerResponseError",
|
|
59
|
+
"Sites",
|
|
60
|
+
"Subscriptions",
|
|
61
|
+
"Tables",
|
|
62
|
+
"Tags",
|
|
63
|
+
"Tasks",
|
|
64
|
+
"Users",
|
|
65
|
+
"Views",
|
|
66
|
+
"VirtualConnections",
|
|
67
|
+
"Webhooks",
|
|
68
|
+
"Workbooks",
|
|
69
|
+
]
|
|
@@ -4,9 +4,9 @@ import warnings
|
|
|
4
4
|
|
|
5
5
|
from defusedxml.ElementTree import fromstring
|
|
6
6
|
|
|
7
|
-
from .endpoint import Endpoint, api
|
|
8
|
-
from .exceptions import ServerResponseError
|
|
9
|
-
from
|
|
7
|
+
from tableauserverclient.server.endpoint.endpoint import Endpoint, api
|
|
8
|
+
from tableauserverclient.server.endpoint.exceptions import ServerResponseError
|
|
9
|
+
from tableauserverclient.server.request_factory import RequestFactory
|
|
10
10
|
|
|
11
11
|
from tableauserverclient.helpers.logging import logger
|
|
12
12
|
|