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
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
from .default_permissions_endpoint import _DefaultPermissionsEndpoint
|
|
4
|
-
from .endpoint import QuerysetEndpoint, api, XML_CONTENT_TYPE
|
|
5
|
-
from .exceptions import MissingRequiredFieldError
|
|
6
|
-
from .permissions_endpoint import _PermissionsEndpoint
|
|
3
|
+
from tableauserverclient.server.endpoint.default_permissions_endpoint import _DefaultPermissionsEndpoint
|
|
4
|
+
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api, XML_CONTENT_TYPE
|
|
5
|
+
from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
|
|
6
|
+
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
|
|
7
7
|
from tableauserverclient.server import RequestFactory, RequestOptions
|
|
8
8
|
from tableauserverclient.models import ProjectItem, PaginationItem, Resource
|
|
9
9
|
|
|
10
10
|
from typing import List, Optional, Tuple, TYPE_CHECKING
|
|
11
11
|
|
|
12
|
+
from tableauserverclient.server.query import QuerySet
|
|
13
|
+
|
|
12
14
|
if TYPE_CHECKING:
|
|
13
|
-
from
|
|
14
|
-
from
|
|
15
|
+
from tableauserverclient.server.server import Server
|
|
16
|
+
from tableauserverclient.server.request_options import RequestOptions
|
|
15
17
|
|
|
16
18
|
from tableauserverclient.helpers.logging import logger
|
|
17
19
|
|
|
18
20
|
|
|
19
|
-
class Projects(QuerysetEndpoint):
|
|
21
|
+
class Projects(QuerysetEndpoint[ProjectItem]):
|
|
20
22
|
def __init__(self, parent_srv: "Server") -> None:
|
|
21
23
|
super(Projects, self).__init__(parent_srv)
|
|
22
24
|
|
|
@@ -75,17 +77,6 @@ class Projects(QuerysetEndpoint):
|
|
|
75
77
|
def populate_permissions(self, item: ProjectItem) -> None:
|
|
76
78
|
self._permissions.populate(item)
|
|
77
79
|
|
|
78
|
-
@api(version="2.0")
|
|
79
|
-
def update_permission(self, item, rules):
|
|
80
|
-
import warnings
|
|
81
|
-
|
|
82
|
-
warnings.warn(
|
|
83
|
-
"Server.projects.update_permission is deprecated, "
|
|
84
|
-
"please use Server.projects.update_permissions instead.",
|
|
85
|
-
DeprecationWarning,
|
|
86
|
-
)
|
|
87
|
-
return self._permissions.update(item, rules)
|
|
88
|
-
|
|
89
80
|
@api(version="2.0")
|
|
90
81
|
def update_permissions(self, item, rules):
|
|
91
82
|
return self._permissions.update(item, rules)
|
|
@@ -165,3 +156,44 @@ class Projects(QuerysetEndpoint):
|
|
|
165
156
|
@api(version="3.4")
|
|
166
157
|
def delete_lens_default_permissions(self, item, rule):
|
|
167
158
|
self._default_permissions.delete_default_permission(item, rule, Resource.Lens)
|
|
159
|
+
|
|
160
|
+
def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[ProjectItem]:
|
|
161
|
+
"""
|
|
162
|
+
Queries the Tableau Server for items using the specified filters. Page
|
|
163
|
+
size can be specified to limit the number of items returned in a single
|
|
164
|
+
request. If not specified, the default page size is 100. Page size can
|
|
165
|
+
be an integer between 1 and 1000.
|
|
166
|
+
|
|
167
|
+
No positional arguments are allowed. All filters must be specified as
|
|
168
|
+
keyword arguments. If you use the equality operator, you can specify it
|
|
169
|
+
through <field_name>=<value>. If you want to use a different operator,
|
|
170
|
+
you can specify it through <field_name>__<operator>=<value>. Field
|
|
171
|
+
names can either be in snake_case or camelCase.
|
|
172
|
+
|
|
173
|
+
This endpoint supports the following fields and operators:
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
created_at=...
|
|
177
|
+
created_at__gt=...
|
|
178
|
+
created_at__gte=...
|
|
179
|
+
created_at__lt=...
|
|
180
|
+
created_at__lte=...
|
|
181
|
+
name=...
|
|
182
|
+
name__in=...
|
|
183
|
+
owner_domain=...
|
|
184
|
+
owner_domain__in=...
|
|
185
|
+
owner_email=...
|
|
186
|
+
owner_email__in=...
|
|
187
|
+
owner_name=...
|
|
188
|
+
owner_name__in=...
|
|
189
|
+
parent_project_id=...
|
|
190
|
+
parent_project_id__in=...
|
|
191
|
+
top_level_project=...
|
|
192
|
+
updated_at=...
|
|
193
|
+
updated_at__gt=...
|
|
194
|
+
updated_at__gte=...
|
|
195
|
+
updated_at__lt=...
|
|
196
|
+
updated_at__lte=...
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
return super().filter(*invalid, page_size=page_size, **kwargs)
|
|
@@ -1,14 +1,25 @@
|
|
|
1
|
+
import abc
|
|
1
2
|
import copy
|
|
3
|
+
from typing import Generic, Iterable, Optional, Protocol, Set, TypeVar, Union, TYPE_CHECKING, runtime_checkable
|
|
2
4
|
import urllib.parse
|
|
3
5
|
|
|
4
|
-
from .endpoint import Endpoint
|
|
5
|
-
from .exceptions import ServerResponseError
|
|
6
|
-
from
|
|
6
|
+
from tableauserverclient.server.endpoint.endpoint import Endpoint, api
|
|
7
|
+
from tableauserverclient.server.endpoint.exceptions import ServerResponseError
|
|
8
|
+
from tableauserverclient.server.exceptions import EndpointUnavailableError
|
|
7
9
|
from tableauserverclient.server import RequestFactory
|
|
8
10
|
from tableauserverclient.models import TagItem
|
|
9
11
|
|
|
10
12
|
from tableauserverclient.helpers.logging import logger
|
|
11
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from tableauserverclient.models.column_item import ColumnItem
|
|
16
|
+
from tableauserverclient.models.database_item import DatabaseItem
|
|
17
|
+
from tableauserverclient.models.datasource_item import DatasourceItem
|
|
18
|
+
from tableauserverclient.models.flow_item import FlowItem
|
|
19
|
+
from tableauserverclient.models.table_item import TableItem
|
|
20
|
+
from tableauserverclient.models.workbook_item import WorkbookItem
|
|
21
|
+
from tableauserverclient.server.server import Server
|
|
22
|
+
|
|
12
23
|
|
|
13
24
|
class _ResourceTagger(Endpoint):
|
|
14
25
|
# Add new tags to resource
|
|
@@ -49,3 +60,124 @@ class _ResourceTagger(Endpoint):
|
|
|
49
60
|
resource_item.tags = self._add_tags(baseurl, resource_item.id, add_set)
|
|
50
61
|
resource_item._initial_tags = copy.copy(resource_item.tags)
|
|
51
62
|
logger.info("Updated tags to {0}".format(resource_item.tags))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Response(Protocol):
|
|
66
|
+
content: bytes
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@runtime_checkable
|
|
70
|
+
class Taggable(Protocol):
|
|
71
|
+
tags: Set[str]
|
|
72
|
+
_initial_tags: Set[str]
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def id(self) -> Optional[str]:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
T = TypeVar("T")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TaggingMixin(abc.ABC, Generic[T]):
|
|
83
|
+
parent_srv: "Server"
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
@abc.abstractmethod
|
|
87
|
+
def baseurl(self) -> str:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
@abc.abstractmethod
|
|
91
|
+
def put_request(self, url, request) -> Response:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
@abc.abstractmethod
|
|
95
|
+
def delete_request(self, url) -> None:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
def add_tags(self, item: Union[T, str], tags: Union[Iterable[str], str]) -> Set[str]:
|
|
99
|
+
item_id = getattr(item, "id", item)
|
|
100
|
+
|
|
101
|
+
if not isinstance(item_id, str):
|
|
102
|
+
raise ValueError("ID not found.")
|
|
103
|
+
|
|
104
|
+
if isinstance(tags, str):
|
|
105
|
+
tag_set = set([tags])
|
|
106
|
+
else:
|
|
107
|
+
tag_set = set(tags)
|
|
108
|
+
|
|
109
|
+
url = f"{self.baseurl}/{item_id}/tags"
|
|
110
|
+
add_req = RequestFactory.Tag.add_req(tag_set)
|
|
111
|
+
server_response = self.put_request(url, add_req)
|
|
112
|
+
return TagItem.from_response(server_response.content, self.parent_srv.namespace)
|
|
113
|
+
|
|
114
|
+
def delete_tags(self, item: Union[T, str], tags: Union[Iterable[str], str]) -> None:
|
|
115
|
+
item_id = getattr(item, "id", item)
|
|
116
|
+
|
|
117
|
+
if not isinstance(item_id, str):
|
|
118
|
+
raise ValueError("ID not found.")
|
|
119
|
+
|
|
120
|
+
if isinstance(tags, str):
|
|
121
|
+
tag_set = set([tags])
|
|
122
|
+
else:
|
|
123
|
+
tag_set = set(tags)
|
|
124
|
+
|
|
125
|
+
for tag in tag_set:
|
|
126
|
+
encoded_tag_name = urllib.parse.quote(tag)
|
|
127
|
+
url = f"{self.baseurl}/{item_id}/tags/{encoded_tag_name}"
|
|
128
|
+
self.delete_request(url)
|
|
129
|
+
|
|
130
|
+
def update_tags(self, item: T) -> None:
|
|
131
|
+
if (initial_tags := getattr(item, "_initial_tags", None)) is None:
|
|
132
|
+
raise ValueError(f"{item} does not have initial tags.")
|
|
133
|
+
if (tags := getattr(item, "tags", None)) is None:
|
|
134
|
+
raise ValueError(f"{item} does not have tags.")
|
|
135
|
+
if tags == initial_tags:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
add_set = tags - initial_tags
|
|
139
|
+
remove_set = initial_tags - tags
|
|
140
|
+
self.delete_tags(item, remove_set)
|
|
141
|
+
if add_set:
|
|
142
|
+
tags = self.add_tags(item, add_set)
|
|
143
|
+
setattr(item, "tags", tags)
|
|
144
|
+
|
|
145
|
+
setattr(item, "_initial_tags", copy.copy(tags))
|
|
146
|
+
logger.info(f"Updated tags to {tags}")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
content = Iterable[Union["ColumnItem", "DatabaseItem", "DatasourceItem", "FlowItem", "TableItem", "WorkbookItem"]]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class Tags(Endpoint):
|
|
153
|
+
def __init__(self, parent_srv: "Server"):
|
|
154
|
+
super().__init__(parent_srv)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def baseurl(self):
|
|
158
|
+
return f"{self.parent_srv.baseurl}/tags"
|
|
159
|
+
|
|
160
|
+
@api(version="3.9")
|
|
161
|
+
def batch_add(self, tags: Union[Iterable[str], str], content: content) -> Set[str]:
|
|
162
|
+
if isinstance(tags, str):
|
|
163
|
+
tag_set = set([tags])
|
|
164
|
+
else:
|
|
165
|
+
tag_set = set(tags)
|
|
166
|
+
|
|
167
|
+
url = f"{self.baseurl}:batchCreate"
|
|
168
|
+
batch_create_req = RequestFactory.Tag.batch_create(tag_set, content)
|
|
169
|
+
server_response = self.put_request(url, batch_create_req)
|
|
170
|
+
return TagItem.from_response(server_response.content, self.parent_srv.namespace)
|
|
171
|
+
|
|
172
|
+
@api(version="3.9")
|
|
173
|
+
def batch_delete(self, tags: Union[Iterable[str], str], content: content) -> Set[str]:
|
|
174
|
+
if isinstance(tags, str):
|
|
175
|
+
tag_set = set([tags])
|
|
176
|
+
else:
|
|
177
|
+
tag_set = set(tags)
|
|
178
|
+
|
|
179
|
+
url = f"{self.baseurl}:batchDelete"
|
|
180
|
+
# The batch delete XML is the same as the batch create XML.
|
|
181
|
+
batch_delete_req = RequestFactory.Tag.batch_create(tag_set, content)
|
|
182
|
+
server_response = self.put_request(url, batch_delete_req)
|
|
183
|
+
return TagItem.from_response(server_response.content, self.parent_srv.namespace)
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from typing import Iterable, Set, Union
|
|
2
3
|
|
|
3
|
-
from .dqw_endpoint import _DataQualityWarningEndpoint
|
|
4
|
-
from .endpoint import api, Endpoint
|
|
5
|
-
from .exceptions import MissingRequiredFieldError
|
|
6
|
-
from .permissions_endpoint import _PermissionsEndpoint
|
|
4
|
+
from tableauserverclient.server.endpoint.dqw_endpoint import _DataQualityWarningEndpoint
|
|
5
|
+
from tableauserverclient.server.endpoint.endpoint import api, Endpoint
|
|
6
|
+
from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
|
|
7
|
+
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
|
|
8
|
+
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
|
|
7
9
|
from tableauserverclient.server import RequestFactory
|
|
8
10
|
from tableauserverclient.models import TableItem, ColumnItem, PaginationItem
|
|
9
|
-
from
|
|
11
|
+
from tableauserverclient.server.pager import Pager
|
|
10
12
|
|
|
11
13
|
from tableauserverclient.helpers.logging import logger
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
class Tables(Endpoint):
|
|
16
|
+
class Tables(Endpoint, TaggingMixin[TableItem]):
|
|
15
17
|
def __init__(self, parent_srv):
|
|
16
18
|
super(Tables, self).__init__(parent_srv)
|
|
17
19
|
|
|
@@ -101,16 +103,6 @@ class Tables(Endpoint):
|
|
|
101
103
|
def populate_permissions(self, item):
|
|
102
104
|
self._permissions.populate(item)
|
|
103
105
|
|
|
104
|
-
@api(version="3.5")
|
|
105
|
-
def update_permission(self, item, rules):
|
|
106
|
-
import warnings
|
|
107
|
-
|
|
108
|
-
warnings.warn(
|
|
109
|
-
"Server.tables.update_permission is deprecated, " "please use Server.tables.update_permissions instead.",
|
|
110
|
-
DeprecationWarning,
|
|
111
|
-
)
|
|
112
|
-
return self._permissions.update(item, rules)
|
|
113
|
-
|
|
114
106
|
@api(version="3.5")
|
|
115
107
|
def update_permissions(self, item, rules):
|
|
116
108
|
return self._permissions.update(item, rules)
|
|
@@ -134,3 +126,14 @@ class Tables(Endpoint):
|
|
|
134
126
|
@api(version="3.5")
|
|
135
127
|
def delete_dqw(self, item):
|
|
136
128
|
self._data_quality_warnings.clear(item)
|
|
129
|
+
|
|
130
|
+
@api(version="3.9")
|
|
131
|
+
def add_tags(self, item: Union[TableItem, str], tags: Union[Iterable[str], str]) -> Set[str]:
|
|
132
|
+
return super().add_tags(item, tags)
|
|
133
|
+
|
|
134
|
+
@api(version="3.9")
|
|
135
|
+
def delete_tags(self, item: Union[TableItem, str], tags: Union[Iterable[str], str]) -> None:
|
|
136
|
+
return super().delete_tags(item, tags)
|
|
137
|
+
|
|
138
|
+
def update_tags(self, item: TableItem) -> None: # type: ignore
|
|
139
|
+
raise NotImplementedError("Update tags is not implemented for TableItem")
|
|
@@ -2,6 +2,8 @@ import copy
|
|
|
2
2
|
import logging
|
|
3
3
|
from typing import List, Optional, Tuple
|
|
4
4
|
|
|
5
|
+
from tableauserverclient.server.query import QuerySet
|
|
6
|
+
|
|
5
7
|
from .endpoint import QuerysetEndpoint, api
|
|
6
8
|
from .exceptions import MissingRequiredFieldError, ServerResponseError
|
|
7
9
|
from tableauserverclient.server import RequestFactory, RequestOptions
|
|
@@ -11,7 +13,7 @@ from ..pager import Pager
|
|
|
11
13
|
from tableauserverclient.helpers.logging import logger
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
class Users(QuerysetEndpoint):
|
|
16
|
+
class Users(QuerysetEndpoint[UserItem]):
|
|
15
17
|
@property
|
|
16
18
|
def baseurl(self) -> str:
|
|
17
19
|
return "{0}/sites/{1}/users".format(self.parent_srv.baseurl, self.parent_srv.site_id)
|
|
@@ -166,3 +168,40 @@ class Users(QuerysetEndpoint):
|
|
|
166
168
|
group_item = GroupItem.from_response(server_response.content, self.parent_srv.namespace)
|
|
167
169
|
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
|
|
168
170
|
return group_item, pagination_item
|
|
171
|
+
|
|
172
|
+
def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[UserItem]:
|
|
173
|
+
"""
|
|
174
|
+
Queries the Tableau Server for items using the specified filters. Page
|
|
175
|
+
size can be specified to limit the number of items returned in a single
|
|
176
|
+
request. If not specified, the default page size is 100. Page size can
|
|
177
|
+
be an integer between 1 and 1000.
|
|
178
|
+
|
|
179
|
+
No positional arguments are allowed. All filters must be specified as
|
|
180
|
+
keyword arguments. If you use the equality operator, you can specify it
|
|
181
|
+
through <field_name>=<value>. If you want to use a different operator,
|
|
182
|
+
you can specify it through <field_name>__<operator>=<value>. Field
|
|
183
|
+
names can either be in snake_case or camelCase.
|
|
184
|
+
|
|
185
|
+
This endpoint supports the following fields and operators:
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
domain_name=...
|
|
189
|
+
domain_name__in=...
|
|
190
|
+
friendly_name=...
|
|
191
|
+
friendly_name__in=...
|
|
192
|
+
is_local=...
|
|
193
|
+
last_login=...
|
|
194
|
+
last_login__gt=...
|
|
195
|
+
last_login__gte=...
|
|
196
|
+
last_login__lt=...
|
|
197
|
+
last_login__lte=...
|
|
198
|
+
luid=...
|
|
199
|
+
luid__in=...
|
|
200
|
+
name__cieq=...
|
|
201
|
+
name=...
|
|
202
|
+
name__in=...
|
|
203
|
+
site_role=...
|
|
204
|
+
site_role__in=...
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
return super().filter(*invalid, page_size=page_size, **kwargs)
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from contextlib import closing
|
|
3
3
|
|
|
4
|
-
from .endpoint import QuerysetEndpoint, api
|
|
5
|
-
from .exceptions import MissingRequiredFieldError
|
|
6
|
-
from .permissions_endpoint import _PermissionsEndpoint
|
|
7
|
-
from .resource_tagger import
|
|
4
|
+
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
|
|
5
|
+
from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
|
|
6
|
+
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
|
|
7
|
+
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
|
|
8
|
+
from tableauserverclient.server.query import QuerySet
|
|
9
|
+
|
|
8
10
|
from tableauserverclient.models import ViewItem, PaginationItem
|
|
9
11
|
|
|
10
12
|
from tableauserverclient.helpers.logging import logger
|
|
11
13
|
|
|
12
|
-
from typing import Iterator, List, Optional, Tuple, TYPE_CHECKING
|
|
14
|
+
from typing import Iterable, Iterator, List, Optional, Set, Tuple, TYPE_CHECKING, Union
|
|
13
15
|
|
|
14
16
|
if TYPE_CHECKING:
|
|
15
|
-
from
|
|
17
|
+
from tableauserverclient.server.request_options import (
|
|
16
18
|
RequestOptions,
|
|
17
19
|
CSVRequestOptions,
|
|
18
20
|
PDFRequestOptions,
|
|
@@ -21,10 +23,9 @@ if TYPE_CHECKING:
|
|
|
21
23
|
)
|
|
22
24
|
|
|
23
25
|
|
|
24
|
-
class Views(QuerysetEndpoint):
|
|
26
|
+
class Views(QuerysetEndpoint[ViewItem], TaggingMixin[ViewItem]):
|
|
25
27
|
def __init__(self, parent_srv):
|
|
26
28
|
super(Views, self).__init__(parent_srv)
|
|
27
|
-
self._resource_tagger = _ResourceTagger(parent_srv)
|
|
28
29
|
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
|
|
29
30
|
|
|
30
31
|
# Used because populate_preview_image functionaliy requires workbook endpoint
|
|
@@ -50,12 +51,14 @@ class Views(QuerysetEndpoint):
|
|
|
50
51
|
return all_view_items, pagination_item
|
|
51
52
|
|
|
52
53
|
@api(version="3.1")
|
|
53
|
-
def get_by_id(self, view_id: str) -> ViewItem:
|
|
54
|
+
def get_by_id(self, view_id: str, usage: bool = False) -> ViewItem:
|
|
54
55
|
if not view_id:
|
|
55
56
|
error = "View item missing ID."
|
|
56
57
|
raise MissingRequiredFieldError(error)
|
|
57
58
|
logger.info("Querying single view (ID: {0})".format(view_id))
|
|
58
59
|
url = "{0}/{1}".format(self.baseurl, view_id)
|
|
60
|
+
if usage:
|
|
61
|
+
url += "?includeUsageStatistics=true"
|
|
59
62
|
server_response = self.get_request(url)
|
|
60
63
|
return ViewItem.from_response(server_response.content, self.parent_srv.namespace)[0]
|
|
61
64
|
|
|
@@ -167,7 +170,91 @@ class Views(QuerysetEndpoint):
|
|
|
167
170
|
error = "View item missing ID. View must be retrieved from server first."
|
|
168
171
|
raise MissingRequiredFieldError(error)
|
|
169
172
|
|
|
170
|
-
self.
|
|
173
|
+
self.update_tags(view_item)
|
|
171
174
|
|
|
172
175
|
# Returning view item to stay consistent with datasource/view update functions
|
|
173
176
|
return view_item
|
|
177
|
+
|
|
178
|
+
@api(version="1.0")
|
|
179
|
+
def add_tags(self, item: Union[ViewItem, str], tags: Union[Iterable[str], str]) -> Set[str]:
|
|
180
|
+
return super().add_tags(item, tags)
|
|
181
|
+
|
|
182
|
+
@api(version="1.0")
|
|
183
|
+
def delete_tags(self, item: Union[ViewItem, str], tags: Union[Iterable[str], str]) -> None:
|
|
184
|
+
return super().delete_tags(item, tags)
|
|
185
|
+
|
|
186
|
+
@api(version="1.0")
|
|
187
|
+
def update_tags(self, item: ViewItem) -> None:
|
|
188
|
+
return super().update_tags(item)
|
|
189
|
+
|
|
190
|
+
def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[ViewItem]:
|
|
191
|
+
"""
|
|
192
|
+
Queries the Tableau Server for items using the specified filters. Page
|
|
193
|
+
size can be specified to limit the number of items returned in a single
|
|
194
|
+
request. If not specified, the default page size is 100. Page size can
|
|
195
|
+
be an integer between 1 and 1000.
|
|
196
|
+
|
|
197
|
+
No positional arguments are allowed. All filters must be specified as
|
|
198
|
+
keyword arguments. If you use the equality operator, you can specify it
|
|
199
|
+
through <field_name>=<value>. If you want to use a different operator,
|
|
200
|
+
you can specify it through <field_name>__<operator>=<value>. Field
|
|
201
|
+
names can either be in snake_case or camelCase.
|
|
202
|
+
|
|
203
|
+
This endpoint supports the following fields and operators:
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
caption=...
|
|
207
|
+
caption__in=...
|
|
208
|
+
content_url=...
|
|
209
|
+
content_url__in=...
|
|
210
|
+
created_at=...
|
|
211
|
+
created_at__gt=...
|
|
212
|
+
created_at__gte=...
|
|
213
|
+
created_at__lt=...
|
|
214
|
+
created_at__lte=...
|
|
215
|
+
favorites_total=...
|
|
216
|
+
favorites_total__gt=...
|
|
217
|
+
favorites_total__gte=...
|
|
218
|
+
favorites_total__lt=...
|
|
219
|
+
favorites_total__lte=...
|
|
220
|
+
fields=...
|
|
221
|
+
fields__in=...
|
|
222
|
+
hits_total=...
|
|
223
|
+
hits_total__gt=...
|
|
224
|
+
hits_total__gte=...
|
|
225
|
+
hits_total__lt=...
|
|
226
|
+
hits_total__lte=...
|
|
227
|
+
name=...
|
|
228
|
+
name__in=...
|
|
229
|
+
owner_domain=...
|
|
230
|
+
owner_domain__in=...
|
|
231
|
+
owner_email=...
|
|
232
|
+
owner_email__in=...
|
|
233
|
+
owner_name=...
|
|
234
|
+
project_name=...
|
|
235
|
+
project_name__in=...
|
|
236
|
+
sheet_number=...
|
|
237
|
+
sheet_number__gt=...
|
|
238
|
+
sheet_number__gte=...
|
|
239
|
+
sheet_number__lt=...
|
|
240
|
+
sheet_number__lte=...
|
|
241
|
+
sheet_type=...
|
|
242
|
+
sheet_type__in=...
|
|
243
|
+
tags=...
|
|
244
|
+
tags__in=...
|
|
245
|
+
title=...
|
|
246
|
+
title__in=...
|
|
247
|
+
updated_at=...
|
|
248
|
+
updated_at__gt=...
|
|
249
|
+
updated_at__gte=...
|
|
250
|
+
updated_at__lt=...
|
|
251
|
+
updated_at__lte=...
|
|
252
|
+
view_url_name=...
|
|
253
|
+
view_url_name__in=...
|
|
254
|
+
workbook_description=...
|
|
255
|
+
workbook_description__in=...
|
|
256
|
+
workbook_name=...
|
|
257
|
+
workbook_name__in=...
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
return super().filter(*invalid, page_size=page_size, **kwargs)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from functools import partial
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Iterable, List, Optional, Set, TYPE_CHECKING, Tuple, Union
|
|
5
|
+
|
|
6
|
+
from tableauserverclient.models.connection_item import ConnectionItem
|
|
7
|
+
from tableauserverclient.models.pagination_item import PaginationItem
|
|
8
|
+
from tableauserverclient.models.revision_item import RevisionItem
|
|
9
|
+
from tableauserverclient.models.virtual_connection_item import VirtualConnectionItem
|
|
10
|
+
from tableauserverclient.server.request_factory import RequestFactory
|
|
11
|
+
from tableauserverclient.server.request_options import RequestOptions
|
|
12
|
+
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
|
|
13
|
+
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
|
|
14
|
+
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
|
|
15
|
+
from tableauserverclient.server.pager import Pager
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from tableauserverclient.server import Server
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class VirtualConnections(QuerysetEndpoint[VirtualConnectionItem], TaggingMixin):
|
|
22
|
+
def __init__(self, parent_srv: "Server") -> None:
|
|
23
|
+
super().__init__(parent_srv)
|
|
24
|
+
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def baseurl(self) -> str:
|
|
28
|
+
return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/virtualConnections"
|
|
29
|
+
|
|
30
|
+
@api(version="3.18")
|
|
31
|
+
def get(self, req_options: Optional[RequestOptions] = None) -> Tuple[List[VirtualConnectionItem], PaginationItem]:
|
|
32
|
+
server_response = self.get_request(self.baseurl, req_options)
|
|
33
|
+
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
|
|
34
|
+
virtual_connections = VirtualConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
|
|
35
|
+
return virtual_connections, pagination_item
|
|
36
|
+
|
|
37
|
+
@api(version="3.18")
|
|
38
|
+
def populate_connections(self, virtual_connection: VirtualConnectionItem) -> VirtualConnectionItem:
|
|
39
|
+
def _connection_fetcher():
|
|
40
|
+
return Pager(partial(self._get_virtual_database_connections, virtual_connection))
|
|
41
|
+
|
|
42
|
+
virtual_connection._connections = _connection_fetcher
|
|
43
|
+
return virtual_connection
|
|
44
|
+
|
|
45
|
+
def _get_virtual_database_connections(
|
|
46
|
+
self, virtual_connection: VirtualConnectionItem, req_options: Optional[RequestOptions] = None
|
|
47
|
+
) -> Tuple[List[ConnectionItem], PaginationItem]:
|
|
48
|
+
server_response = self.get_request(f"{self.baseurl}/{virtual_connection.id}/connections", req_options)
|
|
49
|
+
connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
|
|
50
|
+
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
|
|
51
|
+
|
|
52
|
+
return connections, pagination_item
|
|
53
|
+
|
|
54
|
+
@api(version="3.18")
|
|
55
|
+
def update_connection_db_connection(
|
|
56
|
+
self, virtual_connection: Union[str, VirtualConnectionItem], connection: ConnectionItem
|
|
57
|
+
) -> ConnectionItem:
|
|
58
|
+
vconn_id = getattr(virtual_connection, "id", virtual_connection)
|
|
59
|
+
url = f"{self.baseurl}/{vconn_id}/connections/{connection.id}/modify"
|
|
60
|
+
xml_request = RequestFactory.VirtualConnection.update_db_connection(connection)
|
|
61
|
+
server_response = self.put_request(url, xml_request)
|
|
62
|
+
return ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0]
|
|
63
|
+
|
|
64
|
+
@api(version="3.23")
|
|
65
|
+
def get_by_id(self, virtual_connection: Union[str, VirtualConnectionItem]) -> VirtualConnectionItem:
|
|
66
|
+
vconn_id = getattr(virtual_connection, "id", virtual_connection)
|
|
67
|
+
url = f"{self.baseurl}/{vconn_id}"
|
|
68
|
+
server_response = self.get_request(url)
|
|
69
|
+
return VirtualConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0]
|
|
70
|
+
|
|
71
|
+
@api(version="3.23")
|
|
72
|
+
def download(self, virtual_connection: Union[str, VirtualConnectionItem]) -> str:
|
|
73
|
+
v_conn = self.get_by_id(virtual_connection)
|
|
74
|
+
return json.dumps(v_conn.content)
|
|
75
|
+
|
|
76
|
+
@api(version="3.23")
|
|
77
|
+
def update(self, virtual_connection: VirtualConnectionItem) -> VirtualConnectionItem:
|
|
78
|
+
url = f"{self.baseurl}/{virtual_connection.id}"
|
|
79
|
+
xml_request = RequestFactory.VirtualConnection.update(virtual_connection)
|
|
80
|
+
server_response = self.put_request(url, xml_request)
|
|
81
|
+
return VirtualConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0]
|
|
82
|
+
|
|
83
|
+
@api(version="3.23")
|
|
84
|
+
def get_revisions(
|
|
85
|
+
self, virtual_connection: VirtualConnectionItem, req_options: Optional[RequestOptions] = None
|
|
86
|
+
) -> Tuple[List[RevisionItem], PaginationItem]:
|
|
87
|
+
server_response = self.get_request(f"{self.baseurl}/{virtual_connection.id}/revisions", req_options)
|
|
88
|
+
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
|
|
89
|
+
revisions = RevisionItem.from_response(server_response.content, self.parent_srv.namespace, virtual_connection)
|
|
90
|
+
return revisions, pagination_item
|
|
91
|
+
|
|
92
|
+
@api(version="3.23")
|
|
93
|
+
def download_revision(self, virtual_connection: VirtualConnectionItem, revision_number: int) -> str:
|
|
94
|
+
url = f"{self.baseurl}/{virtual_connection.id}/revisions/{revision_number}"
|
|
95
|
+
server_response = self.get_request(url)
|
|
96
|
+
virtual_connection = VirtualConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0]
|
|
97
|
+
return json.dumps(virtual_connection.content)
|
|
98
|
+
|
|
99
|
+
@api(version="3.23")
|
|
100
|
+
def delete(self, virtual_connection: Union[VirtualConnectionItem, str]) -> None:
|
|
101
|
+
vconn_id = getattr(virtual_connection, "id", virtual_connection)
|
|
102
|
+
self.delete_request(f"{self.baseurl}/{vconn_id}")
|
|
103
|
+
|
|
104
|
+
@api(version="3.23")
|
|
105
|
+
def publish(
|
|
106
|
+
self,
|
|
107
|
+
virtual_connection: VirtualConnectionItem,
|
|
108
|
+
virtual_connection_content: str,
|
|
109
|
+
mode: str = "CreateNew",
|
|
110
|
+
publish_as_draft: bool = False,
|
|
111
|
+
) -> VirtualConnectionItem:
|
|
112
|
+
"""
|
|
113
|
+
Publish a virtual connection to the server.
|
|
114
|
+
|
|
115
|
+
For the virtual_connection object, name, project_id, and owner_id are
|
|
116
|
+
required.
|
|
117
|
+
|
|
118
|
+
The virtual_connection_content can be a json string or a file path to a
|
|
119
|
+
json file.
|
|
120
|
+
|
|
121
|
+
The mode can be "CreateNew" or "Overwrite". If mode is
|
|
122
|
+
"Overwrite" and the virtual connection already exists, it will be
|
|
123
|
+
overwritten.
|
|
124
|
+
|
|
125
|
+
If publish_as_draft is True, the virtual connection will be published
|
|
126
|
+
as a draft, and the id of the draft will be on the response object.
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
json.loads(virtual_connection_content)
|
|
130
|
+
except json.JSONDecodeError:
|
|
131
|
+
file = Path(virtual_connection_content)
|
|
132
|
+
if not file.exists():
|
|
133
|
+
raise RuntimeError(f"{virtual_connection_content} is not valid json nor an existing file path")
|
|
134
|
+
content = file.read_text()
|
|
135
|
+
else:
|
|
136
|
+
content = virtual_connection_content
|
|
137
|
+
|
|
138
|
+
if mode not in ["CreateNew", "Overwrite"]:
|
|
139
|
+
raise ValueError(f"Invalid mode: {mode}")
|
|
140
|
+
overwrite = mode == "Overwrite"
|
|
141
|
+
|
|
142
|
+
url = f"{self.baseurl}?overwrite={str(overwrite).lower()}&publishAsDraft={str(publish_as_draft).lower()}"
|
|
143
|
+
xml_request = RequestFactory.VirtualConnection.publish(virtual_connection, content)
|
|
144
|
+
server_response = self.post_request(url, xml_request)
|
|
145
|
+
return VirtualConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0]
|
|
146
|
+
|
|
147
|
+
@api(version="3.22")
|
|
148
|
+
def populate_permissions(self, item: VirtualConnectionItem) -> None:
|
|
149
|
+
self._permissions.populate(item)
|
|
150
|
+
|
|
151
|
+
@api(version="3.22")
|
|
152
|
+
def add_permissions(self, resource, rules):
|
|
153
|
+
return self._permissions.update(resource, rules)
|
|
154
|
+
|
|
155
|
+
@api(version="3.22")
|
|
156
|
+
def delete_permission(self, item, capability_item):
|
|
157
|
+
return self._permissions.delete(item, capability_item)
|
|
158
|
+
|
|
159
|
+
@api(version="3.23")
|
|
160
|
+
def add_tags(
|
|
161
|
+
self, virtual_connection: Union[VirtualConnectionItem, str], tags: Union[Iterable[str], str]
|
|
162
|
+
) -> Set[str]:
|
|
163
|
+
return super().add_tags(virtual_connection, tags)
|
|
164
|
+
|
|
165
|
+
@api(version="3.23")
|
|
166
|
+
def delete_tags(
|
|
167
|
+
self, virtual_connection: Union[VirtualConnectionItem, str], tags: Union[Iterable[str], str]
|
|
168
|
+
) -> None:
|
|
169
|
+
return super().delete_tags(virtual_connection, tags)
|
|
170
|
+
|
|
171
|
+
@api(version="3.23")
|
|
172
|
+
def update_tags(self, virtual_connection: VirtualConnectionItem) -> None:
|
|
173
|
+
raise NotImplementedError("Update tags is not implemented for Virtual Connections")
|