tableauserverclient 0.32__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.
Files changed (45) hide show
  1. tableauserverclient/__init__.py +10 -0
  2. tableauserverclient/_version.py +3 -3
  3. tableauserverclient/config.py +16 -4
  4. tableauserverclient/models/__init__.py +12 -0
  5. tableauserverclient/models/connection_item.py +4 -2
  6. tableauserverclient/models/database_item.py +6 -0
  7. tableauserverclient/models/flow_item.py +6 -6
  8. tableauserverclient/models/groupset_item.py +53 -0
  9. tableauserverclient/models/interval_item.py +27 -14
  10. tableauserverclient/models/job_item.py +18 -2
  11. tableauserverclient/models/linked_tasks_item.py +102 -0
  12. tableauserverclient/models/permissions_item.py +7 -4
  13. tableauserverclient/models/tableau_types.py +9 -7
  14. tableauserverclient/models/virtual_connection_item.py +77 -0
  15. tableauserverclient/server/endpoint/__init__.py +8 -0
  16. tableauserverclient/server/endpoint/auth_endpoint.py +3 -3
  17. tableauserverclient/server/endpoint/custom_views_endpoint.py +65 -4
  18. tableauserverclient/server/endpoint/databases_endpoint.py +21 -7
  19. tableauserverclient/server/endpoint/datasources_endpoint.py +105 -9
  20. tableauserverclient/server/endpoint/endpoint.py +32 -14
  21. tableauserverclient/server/endpoint/fileuploads_endpoint.py +2 -2
  22. tableauserverclient/server/endpoint/flow_runs_endpoint.py +44 -4
  23. tableauserverclient/server/endpoint/flows_endpoint.py +43 -6
  24. tableauserverclient/server/endpoint/groups_endpoint.py +82 -14
  25. tableauserverclient/server/endpoint/groupsets_endpoint.py +127 -0
  26. tableauserverclient/server/endpoint/jobs_endpoint.py +74 -7
  27. tableauserverclient/server/endpoint/linked_tasks_endpoint.py +45 -0
  28. tableauserverclient/server/endpoint/projects_endpoint.py +43 -0
  29. tableauserverclient/server/endpoint/resource_tagger.py +135 -3
  30. tableauserverclient/server/endpoint/tables_endpoint.py +19 -6
  31. tableauserverclient/server/endpoint/users_endpoint.py +39 -0
  32. tableauserverclient/server/endpoint/views_endpoint.py +94 -9
  33. tableauserverclient/server/endpoint/virtual_connections_endpoint.py +173 -0
  34. tableauserverclient/server/endpoint/workbooks_endpoint.py +91 -10
  35. tableauserverclient/server/pager.py +6 -7
  36. tableauserverclient/server/query.py +2 -1
  37. tableauserverclient/server/request_factory.py +178 -7
  38. tableauserverclient/server/request_options.py +4 -2
  39. tableauserverclient/server/server.py +8 -0
  40. {tableauserverclient-0.32.dist-info → tableauserverclient-0.33.dist-info}/METADATA +15 -15
  41. {tableauserverclient-0.32.dist-info → tableauserverclient-0.33.dist-info}/RECORD +45 -39
  42. {tableauserverclient-0.32.dist-info → tableauserverclient-0.33.dist-info}/WHEEL +1 -1
  43. {tableauserverclient-0.32.dist-info → tableauserverclient-0.33.dist-info}/LICENSE +0 -0
  44. {tableauserverclient-0.32.dist-info → tableauserverclient-0.33.dist-info}/LICENSE.versioneer +0 -0
  45. {tableauserverclient-0.32.dist-info → tableauserverclient-0.33.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,19 @@
1
1
  import logging
2
2
 
3
- from .endpoint import QuerysetEndpoint, api
4
- from .exceptions import MissingRequiredFieldError
3
+ from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
4
+ from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
5
5
  from tableauserverclient.server import RequestFactory
6
6
  from tableauserverclient.models import GroupItem, UserItem, PaginationItem, JobItem
7
- from ..pager import Pager
7
+ from tableauserverclient.server.pager import Pager
8
8
 
9
9
  from tableauserverclient.helpers.logging import logger
10
10
 
11
- from typing import List, Optional, TYPE_CHECKING, Tuple, Union
11
+ from typing import Iterable, List, Optional, TYPE_CHECKING, Tuple, Union
12
+
13
+ from tableauserverclient.server.query import QuerySet
12
14
 
13
15
  if TYPE_CHECKING:
14
- from ..request_options import RequestOptions
16
+ from tableauserverclient.server.request_options import RequestOptions
15
17
 
16
18
 
17
19
  class Groups(QuerysetEndpoint[GroupItem]):
@@ -19,9 +21,9 @@ class Groups(QuerysetEndpoint[GroupItem]):
19
21
  def baseurl(self) -> str:
20
22
  return "{0}/sites/{1}/groups".format(self.parent_srv.baseurl, self.parent_srv.site_id)
21
23
 
22
- # Gets all groups
23
24
  @api(version="2.0")
24
25
  def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[GroupItem], PaginationItem]:
26
+ """Gets all groups"""
25
27
  logger.info("Querying all groups on site")
26
28
  url = self.baseurl
27
29
  server_response = self.get_request(url, req_options)
@@ -29,9 +31,9 @@ class Groups(QuerysetEndpoint[GroupItem]):
29
31
  all_group_items = GroupItem.from_response(server_response.content, self.parent_srv.namespace)
30
32
  return all_group_items, pagination_item
31
33
 
32
- # Gets all users in a given group
33
34
  @api(version="2.0")
34
- def populate_users(self, group_item, req_options: Optional["RequestOptions"] = None) -> None:
35
+ def populate_users(self, group_item: GroupItem, req_options: Optional["RequestOptions"] = None) -> None:
36
+ """Gets all users in a given group"""
35
37
  if not group_item.id:
36
38
  error = "Group item missing ID. Group must be retrieved from server first."
37
39
  raise MissingRequiredFieldError(error)
@@ -47,7 +49,7 @@ class Groups(QuerysetEndpoint[GroupItem]):
47
49
  group_item._set_users(user_pager)
48
50
 
49
51
  def _get_users_for_group(
50
- self, group_item, req_options: Optional["RequestOptions"] = None
52
+ self, group_item: GroupItem, req_options: Optional["RequestOptions"] = None
51
53
  ) -> Tuple[List[UserItem], PaginationItem]:
52
54
  url = "{0}/{1}/users".format(self.baseurl, group_item.id)
53
55
  server_response = self.get_request(url, req_options)
@@ -56,9 +58,9 @@ class Groups(QuerysetEndpoint[GroupItem]):
56
58
  logger.info("Populated users for group (ID: {0})".format(group_item.id))
57
59
  return user_item, pagination_item
58
60
 
59
- # Deletes 1 group by id
60
61
  @api(version="2.0")
61
62
  def delete(self, group_id: str) -> None:
63
+ """Deletes 1 group by id"""
62
64
  if not group_id:
63
65
  error = "Group ID undefined."
64
66
  raise ValueError(error)
@@ -87,17 +89,17 @@ class Groups(QuerysetEndpoint[GroupItem]):
87
89
  else:
88
90
  return GroupItem.from_response(server_response.content, self.parent_srv.namespace)[0]
89
91
 
90
- # Create a 'local' Tableau group
91
92
  @api(version="2.0")
92
93
  def create(self, group_item: GroupItem) -> GroupItem:
94
+ """Create a 'local' Tableau group"""
93
95
  url = self.baseurl
94
96
  create_req = RequestFactory.Group.create_local_req(group_item)
95
97
  server_response = self.post_request(url, create_req)
96
98
  return GroupItem.from_response(server_response.content, self.parent_srv.namespace)[0]
97
99
 
98
- # Create a group based on Active Directory
99
100
  @api(version="2.0")
100
101
  def create_AD_group(self, group_item: GroupItem, asJob: bool = False) -> Union[GroupItem, JobItem]:
102
+ """Create a group based on Active Directory"""
101
103
  asJobparameter = "?asJob=true" if asJob else ""
102
104
  url = self.baseurl + asJobparameter
103
105
  create_req = RequestFactory.Group.create_ad_req(group_item)
@@ -107,9 +109,9 @@ class Groups(QuerysetEndpoint[GroupItem]):
107
109
  else:
108
110
  return GroupItem.from_response(server_response.content, self.parent_srv.namespace)[0]
109
111
 
110
- # Removes 1 user from 1 group
111
112
  @api(version="2.0")
112
113
  def remove_user(self, group_item: GroupItem, user_id: str) -> None:
114
+ """Removes 1 user from 1 group"""
113
115
  if not group_item.id:
114
116
  error = "Group item missing ID."
115
117
  raise MissingRequiredFieldError(error)
@@ -120,9 +122,22 @@ class Groups(QuerysetEndpoint[GroupItem]):
120
122
  self.delete_request(url)
121
123
  logger.info("Removed user (id: {0}) from group (ID: {1})".format(user_id, group_item.id))
122
124
 
123
- # Adds 1 user to 1 group
125
+ @api(version="3.21")
126
+ def remove_users(self, group_item: GroupItem, users: Iterable[Union[str, UserItem]]) -> None:
127
+ """Removes multiple users from 1 group"""
128
+ group_id = group_item.id if hasattr(group_item, "id") else group_item
129
+ if not isinstance(group_id, str):
130
+ raise ValueError(f"Invalid group provided: {group_item}")
131
+
132
+ url = f"{self.baseurl}/{group_id}/users/remove"
133
+ add_req = RequestFactory.Group.remove_users_req(users)
134
+ _ = self.put_request(url, add_req)
135
+ logger.info("Removed users to group (ID: {0})".format(group_item.id))
136
+ return None
137
+
124
138
  @api(version="2.0")
125
139
  def add_user(self, group_item: GroupItem, user_id: str) -> UserItem:
140
+ """Adds 1 user to 1 group"""
126
141
  if not group_item.id:
127
142
  error = "Group item missing ID."
128
143
  raise MissingRequiredFieldError(error)
@@ -135,3 +150,56 @@ class Groups(QuerysetEndpoint[GroupItem]):
135
150
  user = UserItem.from_response(server_response.content, self.parent_srv.namespace).pop()
136
151
  logger.info("Added user (id: {0}) to group (ID: {1})".format(user_id, group_item.id))
137
152
  return user
153
+
154
+ @api(version="3.21")
155
+ def add_users(self, group_item: GroupItem, users: Iterable[Union[str, UserItem]]) -> List[UserItem]:
156
+ """Adds multiple users to 1 group"""
157
+ group_id = group_item.id if hasattr(group_item, "id") else group_item
158
+ if not isinstance(group_id, str):
159
+ raise ValueError(f"Invalid group provided: {group_item}")
160
+
161
+ url = f"{self.baseurl}/{group_id}/users"
162
+ add_req = RequestFactory.Group.add_users_req(users)
163
+ server_response = self.post_request(url, add_req)
164
+ users = UserItem.from_response(server_response.content, self.parent_srv.namespace)
165
+ logger.info("Added users to group (ID: {0})".format(group_item.id))
166
+ return users
167
+
168
+ def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[GroupItem]:
169
+ """
170
+ Queries the Tableau Server for items using the specified filters. Page
171
+ size can be specified to limit the number of items returned in a single
172
+ request. If not specified, the default page size is 100. Page size can
173
+ be an integer between 1 and 1000.
174
+
175
+ No positional arguments are allowed. All filters must be specified as
176
+ keyword arguments. If you use the equality operator, you can specify it
177
+ through <field_name>=<value>. If you want to use a different operator,
178
+ you can specify it through <field_name>__<operator>=<value>. Field
179
+ names can either be in snake_case or camelCase.
180
+
181
+ This endpoint supports the following fields and operators:
182
+
183
+
184
+ domain_name=...
185
+ domain_name__in=...
186
+ domain_nickname=...
187
+ domain_nickname__in=...
188
+ is_external_user_enabled=...
189
+ is_local=...
190
+ luid=...
191
+ luid__in=...
192
+ minimum_site_role=...
193
+ minimum_site_role__in=...
194
+ name__cieq=...
195
+ name=...
196
+ name__in=...
197
+ name__like=...
198
+ user_count=...
199
+ user_count__gt=...
200
+ user_count__gte=...
201
+ user_count__lt=...
202
+ user_count__lte=...
203
+ """
204
+
205
+ return super().filter(*invalid, page_size=page_size, **kwargs)
@@ -0,0 +1,127 @@
1
+ from typing import List, Literal, Optional, Tuple, TYPE_CHECKING, Union
2
+
3
+ from tableauserverclient.helpers.logging import logger
4
+ from tableauserverclient.models.group_item import GroupItem
5
+ from tableauserverclient.models.groupset_item import GroupSetItem
6
+ from tableauserverclient.models.pagination_item import PaginationItem
7
+ from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint
8
+ from tableauserverclient.server.query import QuerySet
9
+ from tableauserverclient.server.request_options import RequestOptions
10
+ from tableauserverclient.server.request_factory import RequestFactory
11
+ from tableauserverclient.server.endpoint.endpoint import api
12
+
13
+ if TYPE_CHECKING:
14
+ from tableauserverclient.server import Server
15
+
16
+
17
+ class GroupSets(QuerysetEndpoint[GroupSetItem]):
18
+ def __init__(self, parent_srv: "Server") -> None:
19
+ super().__init__(parent_srv)
20
+
21
+ @property
22
+ def baseurl(self) -> str:
23
+ return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/groupsets"
24
+
25
+ @api(version="3.22")
26
+ def get(
27
+ self,
28
+ request_options: Optional[RequestOptions] = None,
29
+ result_level: Optional[Literal["members", "local"]] = None,
30
+ ) -> Tuple[List[GroupSetItem], PaginationItem]:
31
+ logger.info("Querying all group sets on site")
32
+ url = self.baseurl
33
+ if result_level:
34
+ url += f"?resultlevel={result_level}"
35
+ server_response = self.get_request(url, request_options)
36
+ pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
37
+ all_group_set_items = GroupSetItem.from_response(server_response.content, self.parent_srv.namespace)
38
+ return all_group_set_items, pagination_item
39
+
40
+ @api(version="3.22")
41
+ def get_by_id(self, groupset_id: str) -> GroupSetItem:
42
+ logger.info(f"Querying group set (ID: {groupset_id})")
43
+ url = f"{self.baseurl}/{groupset_id}"
44
+ server_response = self.get_request(url)
45
+ all_group_set_items = GroupSetItem.from_response(server_response.content, self.parent_srv.namespace)
46
+ return all_group_set_items[0]
47
+
48
+ @api(version="3.22")
49
+ def create(self, groupset_item: GroupSetItem) -> GroupSetItem:
50
+ logger.info(f"Creating group set (name: {groupset_item.name})")
51
+ url = self.baseurl
52
+ request = RequestFactory.GroupSet.create_request(groupset_item)
53
+ server_response = self.post_request(url, request)
54
+ created_groupset = GroupSetItem.from_response(server_response.content, self.parent_srv.namespace)
55
+ return created_groupset[0]
56
+
57
+ @api(version="3.22")
58
+ def add_group(self, groupset_item: GroupSetItem, group: Union[GroupItem, str]) -> None:
59
+ group_id = group.id if isinstance(group, GroupItem) else group
60
+ logger.info(f"Adding group (ID: {group_id}) to group set (ID: {groupset_item.id})")
61
+ url = f"{self.baseurl}/{groupset_item.id}/groups/{group_id}"
62
+ _ = self.put_request(url)
63
+ return None
64
+
65
+ @api(version="3.22")
66
+ def remove_group(self, groupset_item: GroupSetItem, group: Union[GroupItem, str]) -> None:
67
+ group_id = group.id if isinstance(group, GroupItem) else group
68
+ logger.info(f"Removing group (ID: {group_id}) from group set (ID: {groupset_item.id})")
69
+ url = f"{self.baseurl}/{groupset_item.id}/groups/{group_id}"
70
+ _ = self.delete_request(url)
71
+ return None
72
+
73
+ @api(version="3.22")
74
+ def delete(self, groupset: Union[GroupSetItem, str]) -> None:
75
+ groupset_id = groupset.id if isinstance(groupset, GroupSetItem) else groupset
76
+ logger.info(f"Deleting group set (ID: {groupset_id})")
77
+ url = f"{self.baseurl}/{groupset_id}"
78
+ _ = self.delete_request(url)
79
+ return None
80
+
81
+ @api(version="3.22")
82
+ def update(self, groupset: GroupSetItem) -> GroupSetItem:
83
+ logger.info(f"Updating group set (ID: {groupset.id})")
84
+ url = f"{self.baseurl}/{groupset.id}"
85
+ request = RequestFactory.GroupSet.update_request(groupset)
86
+ server_response = self.put_request(url, request)
87
+ updated_groupset = GroupSetItem.from_response(server_response.content, self.parent_srv.namespace)
88
+ return updated_groupset[0]
89
+
90
+ def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[GroupSetItem]:
91
+ """
92
+ Queries the Tableau Server for items using the specified filters. Page
93
+ size can be specified to limit the number of items returned in a single
94
+ request. If not specified, the default page size is 100. Page size can
95
+ be an integer between 1 and 1000.
96
+
97
+ No positional arguments are allowed. All filters must be specified as
98
+ keyword arguments. If you use the equality operator, you can specify it
99
+ through <field_name>=<value>. If you want to use a different operator,
100
+ you can specify it through <field_name>__<operator>=<value>. Field
101
+ names can either be in snake_case or camelCase.
102
+
103
+ This endpoint supports the following fields and operators:
104
+
105
+
106
+ domain_name=...
107
+ domain_name__in=...
108
+ domain_nickname=...
109
+ domain_nickname__in=...
110
+ is_external_user_enabled=...
111
+ is_local=...
112
+ luid=...
113
+ luid__in=...
114
+ minimum_site_role=...
115
+ minimum_site_role__in=...
116
+ name__cieq=...
117
+ name=...
118
+ name__in=...
119
+ name__like=...
120
+ user_count=...
121
+ user_count__gt=...
122
+ user_count__gte=...
123
+ user_count__lt=...
124
+ user_count__lte=...
125
+ """
126
+
127
+ return super().filter(*invalid, page_size=page_size, **kwargs)
@@ -1,9 +1,12 @@
1
1
  import logging
2
+ from typing_extensions import Self, overload
3
+
2
4
 
3
- from .endpoint import QuerysetEndpoint, api
4
- from .exceptions import JobCancelledException, JobFailedException
5
5
  from tableauserverclient.models import JobItem, BackgroundJobItem, PaginationItem
6
- from ..request_options import RequestOptionsBase
6
+ from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
7
+ from tableauserverclient.server.endpoint.exceptions import JobCancelledException, JobFailedException
8
+ from tableauserverclient.server.query import QuerySet
9
+ from tableauserverclient.server.request_options import RequestOptionsBase
7
10
  from tableauserverclient.exponential_backoff import ExponentialBackoffTimer
8
11
 
9
12
  from tableauserverclient.helpers.logging import logger
@@ -11,15 +14,25 @@ from tableauserverclient.helpers.logging import logger
11
14
  from typing import List, Optional, Tuple, Union
12
15
 
13
16
 
14
- class Jobs(QuerysetEndpoint[JobItem]):
17
+ class Jobs(QuerysetEndpoint[BackgroundJobItem]):
15
18
  @property
16
19
  def baseurl(self):
17
20
  return "{0}/sites/{1}/jobs".format(self.parent_srv.baseurl, self.parent_srv.site_id)
18
21
 
22
+ @overload # type: ignore[override]
23
+ def get(self: Self, job_id: str, req_options: Optional[RequestOptionsBase] = None) -> JobItem: # type: ignore[override]
24
+ ...
25
+
26
+ @overload # type: ignore[override]
27
+ def get(self: Self, job_id: RequestOptionsBase, req_options: None) -> Tuple[List[BackgroundJobItem], PaginationItem]: # type: ignore[override]
28
+ ...
29
+
30
+ @overload # type: ignore[override]
31
+ def get(self: Self, job_id: None, req_options: Optional[RequestOptionsBase]) -> Tuple[List[BackgroundJobItem], PaginationItem]: # type: ignore[override]
32
+ ...
33
+
19
34
  @api(version="2.6")
20
- def get(
21
- self, job_id: Optional[str] = None, req_options: Optional[RequestOptionsBase] = None
22
- ) -> Tuple[List[BackgroundJobItem], PaginationItem]:
35
+ def get(self, job_id=None, req_options=None):
23
36
  # Backwards Compatibility fix until we rev the major version
24
37
  if job_id is not None and isinstance(job_id, str):
25
38
  import warnings
@@ -74,3 +87,57 @@ class Jobs(QuerysetEndpoint[JobItem]):
74
87
  raise JobCancelledException(job)
75
88
  else:
76
89
  raise AssertionError("Unexpected finish_code in job", job)
90
+
91
+ def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[BackgroundJobItem]:
92
+ """
93
+ Queries the Tableau Server for items using the specified filters. Page
94
+ size can be specified to limit the number of items returned in a single
95
+ request. If not specified, the default page size is 100. Page size can
96
+ be an integer between 1 and 1000.
97
+
98
+ No positional arguments are allowed. All filters must be specified as
99
+ keyword arguments. If you use the equality operator, you can specify it
100
+ through <field_name>=<value>. If you want to use a different operator,
101
+ you can specify it through <field_name>__<operator>=<value>. Field
102
+ names can either be in snake_case or camelCase.
103
+
104
+ This endpoint supports the following fields and operators:
105
+
106
+
107
+ args__has=...
108
+ completed_at=...
109
+ completed_at__gt=...
110
+ completed_at__gte=...
111
+ completed_at__lt=...
112
+ completed_at__lte=...
113
+ created_at=...
114
+ created_at__gt=...
115
+ created_at__gte=...
116
+ created_at__lt=...
117
+ created_at__lte=...
118
+ job_type=...
119
+ job_type__in=...
120
+ notes__has=...
121
+ priority=...
122
+ priority__gt=...
123
+ priority__gte=...
124
+ priority__lt=...
125
+ priority__lte=...
126
+ progress=...
127
+ progress__gt=...
128
+ progress__gte=...
129
+ progress__lt=...
130
+ progress__lte=...
131
+ started_at=...
132
+ started_at__gt=...
133
+ started_at__gte=...
134
+ started_at__lt=...
135
+ started_at__lte=...
136
+ status=...
137
+ subtitle=...
138
+ subtitle__has=...
139
+ title=...
140
+ title__has=...
141
+ """
142
+
143
+ return super().filter(*invalid, page_size=page_size, **kwargs)
@@ -0,0 +1,45 @@
1
+ from typing import List, Optional, Tuple, Union
2
+
3
+ from tableauserverclient.helpers.logging import logger
4
+ from tableauserverclient.models.linked_tasks_item import LinkedTaskItem, LinkedTaskJobItem
5
+ from tableauserverclient.models.pagination_item import PaginationItem
6
+ from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
7
+ from tableauserverclient.server.request_factory import RequestFactory
8
+ from tableauserverclient.server.request_options import RequestOptions
9
+
10
+
11
+ class LinkedTasks(QuerysetEndpoint[LinkedTaskItem]):
12
+ def __init__(self, parent_srv):
13
+ super().__init__(parent_srv)
14
+ self._parent_srv = parent_srv
15
+
16
+ @property
17
+ def baseurl(self) -> str:
18
+ return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/tasks/linked"
19
+
20
+ @api(version="3.15")
21
+ def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[LinkedTaskItem], PaginationItem]:
22
+ logger.info("Querying all linked tasks on site")
23
+ url = self.baseurl
24
+ server_response = self.get_request(url, req_options)
25
+ pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
26
+ all_group_items = LinkedTaskItem.from_response(server_response.content, self.parent_srv.namespace)
27
+ return all_group_items, pagination_item
28
+
29
+ @api(version="3.15")
30
+ def get_by_id(self, linked_task: Union[LinkedTaskItem, str]) -> LinkedTaskItem:
31
+ task_id = getattr(linked_task, "id", linked_task)
32
+ logger.info("Querying all linked tasks on site")
33
+ url = f"{self.baseurl}/{task_id}"
34
+ server_response = self.get_request(url)
35
+ all_group_items = LinkedTaskItem.from_response(server_response.content, self.parent_srv.namespace)
36
+ return all_group_items[0]
37
+
38
+ @api(version="3.15")
39
+ def run_now(self, linked_task: Union[LinkedTaskItem, str]) -> LinkedTaskJobItem:
40
+ task_id = getattr(linked_task, "id", linked_task)
41
+ logger.info(f"Running linked task {task_id} now")
42
+ url = f"{self.baseurl}/{task_id}/runNow"
43
+ empty_req = RequestFactory.Empty.empty_req()
44
+ server_response = self.post_request(url, empty_req)
45
+ return LinkedTaskJobItem.from_response(server_response.content, self.parent_srv.namespace)
@@ -9,6 +9,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
15
  from tableauserverclient.server.server import Server
14
16
  from tableauserverclient.server.request_options import RequestOptions
@@ -154,3 +156,44 @@ class Projects(QuerysetEndpoint[ProjectItem]):
154
156
  @api(version="3.4")
155
157
  def delete_lens_default_permissions(self, item, rule):
156
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 ..exceptions import EndpointUnavailableError
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)