8000 Merge pull request #1422 from jorwoods/jorwoods/wb_tags · tableau/server-client-python@9e23d31 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9e23d31

Browse files
authored
Merge pull request #1422 from jorwoods/jorwoods/wb_tags
feat: tag everything
2 parents a3028d7 + 273beb1 commit 9e23d31

11 files changed

+442
-43
lines changed

tableauserverclient/server/endpoint/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from tableauserverclient.server.endpoint.sites_endpoint import Sites
2323
from tableauserverclient.server.endpoint.subscriptions_endpoint import Subscriptions
2424
from tableauserverclient.server.endpoint.tables_endpoint import Tables
25+
from tableauserverclient.server.endpoint.resource_tagger import Tags
2526
from tableauserverclient.server.endpoint.tasks_endpoint import Tasks
2627
from tableauserverclient.server.endpoint.users_endpoint import Users
2728
from tableauserverclient.server.endpoint.views_endpoint import Views
@@ -55,6 +56,7 @@
5556
"Sites",
5657
"Subscriptions",
5758
"Tables",
59+
"Tags",
5860
"Tasks",
5961
"Users",
6062
"Views",

tableauserverclient/server/endpoint/databases_endpoint.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import logging
2-
3-
from .default_permissions_endpoint import _DefaultPermissionsEndpoint
4-
from .dqw_endpoint import _DataQualityWarningEndpoint
5-
from .endpoint import api, Endpoint
6-
from .exceptions import MissingRequiredFieldError
7-
from .permissions_endpoint import _PermissionsEndpoint
2+
from typing import Union, Iterable, Set
3+
4+
from tableauserverclient.server.endpoint.default_permissions_endpoint import _DefaultPermissionsEndpoint
5+
from tableauserverclient.server.endpoint.dqw_endpoint import _DataQualityWarningEndpoint
6+
from tableauserverclient.server.endpoint.endpoint import api, Endpoint
7+
from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
8+
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
9+
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
810
from tableauserverclient.server import RequestFactory
911
from tableauserverclient.models import DatabaseItem, TableItem, PaginationItem, Resource
1012

1113
from tableauserverclient.helpers.logging import logger
1214

1315

14-
class Databases(Endpoint):
16+
class Databases(Endpoint, TaggingMixin):
1517
def __init__(self, parent_srv):
1618
super(Databases, self).__init__(parent_srv)
1719

@@ -123,3 +125,15 @@ def add_dqw(self, item, warning):
123125
@api(version="3.5")
124126
def delete_dqw(self, item):
125127
self._data_quality_warnings.clear(item)
128+
129+
@api(version="3.9")
130+
def add_tags(self, item: Union[DatabaseItem, str], tags: Iterable[str]) -> Set[str]:
131+
return super().add_tags(item, tags)
132+
133+
@api(version="3.9")
134+
def delete_tags(self, item: Union[DatabaseItem, str], tags: Iterable[str]) -> None:
135+
super().delete_tags(item, tags)
136+
137+
@api(version="3.9")
138+
def update_tags(self, item: DatabaseItem) -> None:
139+
raise NotImplementedError("Update tags is not supported for databases.")

tableauserverclient/server/endpoint/datasources_endpoint.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from contextlib import closing
88
from pathlib import Path
9-
from typing import List, Mapping, Optional, Sequence, Tuple, TYPE_CHECKING, Union
9+
from typing import Iterable, List, Mapping, Optional, Sequence, Set, Tuple, TYPE_CHECKING, Union
1010

1111
from tableauserverclient.helpers.headers import fix_filename
1212
from tableauserverclient.server.query import QuerySet
@@ -20,7 +20,7 @@
2020
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api, parameter_added_in
2121
from tableauserverclient.server.endpoint.exceptions import InternalServerError, MissingRequiredFieldError
2222
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
23-
from tableauserverclient.server.endpoint.resource_tagger import _ResourceTagger
23+
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
2424

2525
from tableauserverclient.config import ALLOWED_FILE_EXTENSIONS, FILESIZE_LIMIT_MB, BYTES_PER_MB, CHUNK_SIZE_MB
2626
from tableauserverclient.filesys_helpers import (
@@ -55,10 +55,9 @@
5555
PathOrFileW = Union[FilePath, FileObjectW]
5656

5757

58-
class Datasources(QuerysetEndpoint[DatasourceItem]):
58+
class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin):
5959
def __init__(self, parent_srv: "Server") -> None:
6060
super(Datasources, self).__init__(parent_srv)
61-
self._resource_tagger = _ResourceTagger(parent_srv)
6261
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
6362
self._data_quality_warnings = _DataQualityWarningEndpoint(self.parent_srv, "datasource")
6463

@@ -150,7 +149,7 @@ def update(self, datasource_item: DatasourceItem) -> DatasourceItem:
150149
)
151150
raise MissingRequiredFieldError(error)
152151

153-
self._resource_tagger.update_tags(self.baseurl, datasource_item)
152+
self.update_tags(datasource_item)
154153

155154
# Update the datasource itself
156155
url = "{0}/{1}".format(self.baseurl, datasource_item.id)
@@ -461,6 +460,18 @@ def schedule_extract_refresh(
461460
) -> List["AddResponse"]: # actually should return a task
462461
return self.parent_srv.schedules.add_to_schedule(schedule_id, datasource=item)
463462

463+
@api(version="1.0")
464+
def add_tags(self, item: Union[DatasourceItem, str], tags: Union[Iterable[str], str]) -> Set[str]:
465+
return super().add_tags(item, tags)
466+
467+
@api(version="1.0")
468+
def delete_tags(self, item: Union[DatasourceItem, str], tags: Union[Iterable[str], str]) -> None:
469+
return super().delete_tags(item, tags)
470+
471+
@api(version="1.0")
472+
def update_tags(self, item: DatasourceItem) -> None:
473+
return super().update_tags(item)
474+
464475
def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[DatasourceItem]:
465476
"""
466477
Queries the Tableau Server for items using the specified filters. Page

tableauserverclient/server/endpoint/flows_endpoint.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
1414
from tableauserverclient.server.endpoint.exceptions import InternalServerError, MissingRequiredFieldError
1515
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
16-
from tableauserverclient.server.endpoint.resource_tagger import _ResourceTagger
16+
from tableauserverclient.server.endpoint.resource_tagger import _ResourceTagger, TaggingMixin
1717
from tableauserverclient.models import FlowItem, Paginatio F438 nItem, ConnectionItem, JobItem
1818
from tableauserverclient.server import RequestFactory
1919
from tableauserverclient.filesys_helpers import (
@@ -51,7 +51,7 @@
5151
PathOrFileW = Union[FilePath, FileObjectW]
5252

5353

54-
class Flows(QuerysetEndpoint[FlowItem]):
54+
class Flows(QuerysetEndpoint[FlowItem], TaggingMixin):
5555
def __init__(self, parent_srv):
5656
super(Flows, self).__init__(parent_srv)
5757
self._resource_tagger = _ResourceTagger(parent_srv)

tableauserverclient/server/endpoint/resource_tagger.py

Lines changed: 132 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
1+
import abc
12
import copy
3+
from typing import Iterable, Optional, Protocol, Set, Union, TYPE_CHECKING, runtime_checkable
24
import urllib.parse
35

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
79
from tableauserverclient.server import RequestFactory
810
from tableauserverclient.models import TagItem
911

1012
from tableauserverclient.helpers.logging import logger
1113

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+
1223

1324
class _ResourceTagger(Endpoint):
1425
# Add new tags to resource
@@ -49,3 +60,121 @@ def update_tags(self, baseurl, resource_item):
4960
resource_item.tags = self._add_tags(baseurl, resource_item.id, add_set)
5061
resource_item._initial_tags = copy.copy(resource_item.tags)
5162
logger.info("Updated tags to {0}".format(resource_item.tags))
63+
64+
65+
class HasID(Protocol):
66+
@property
67+
def id(self) -> Optional[str]:
68+
pass
69+
70+
71+
@runtime_checkable
72+
class Taggable(Protocol):
73+
_initial_tags: Set[str]
74+
tags: Set[str]
75+
76+
@property
77+
def id(self) -> Optional[str]:
78+
pass
79+
80+
81+
class Response(Protocol):
82+
content: bytes
83+
84+
85+
class TaggingMixin(abc.ABC):
86+
parent_srv: "Server"
87+
88+
@property
89+
@abc.abstractmethod
90+
def baseurl(self) -> str:
91+
pass
92+
93+
@abc.abstractmethod
94+
def put_request(self, url, request) -> Response:
95+
pass
96+
97+
@abc.abstractmethod
98+
def delete_request(self, url) -> None:
99+
pass
100+
101+
def add_tags(self, item: Union[HasID, Taggable, str], tags: Union[Iterable[str], str]) -> Set[str]:
102+
item_id = getattr(item, "id", item)
103+
104+
if not isinstance(item_id, str):
105+
raise ValueError("ID not found.")
106+
107+
if isinstance(tags, str):
108+
tag_set = set([tags])
109+
else:
110+
tag_set = set(tags)
111+
112+
url = f"{self.baseurl}/{item_id}/tags"
113+
add_req = RequestFactory.Tag.add_req(tag_set)
114+
server_response = self.put_request(url, add_req)
115+
return TagItem.from_response(server_response.content, self.parent_srv.namespace)
116+
117+
def delete_tags(self, item: Union[HasID, Taggable, str], tags: Union[Iterable[str], str]) -> None:
118+
item_id = getattr(item, "id", item)
119+
120+
if not isinstance(item_id, str):
121+
raise ValueError("ID not found.")
122+
123+
if isinstance(tags, str):
124+
tag_set = set([tags])
125+
else:
126+
tag_set = set(tags)
127+
128+
for tag in tag_set:
129+
encoded_tag_name = urllib.parse.quote(tag)
130+
url = f"{self.baseurl}/{item_id}/tags/{encoded_tag_name}"
131+
self.delete_request(url)
132+
133+
def update_tags(self, item: Taggable) -> None:
134+
if item.tags == item._initial_tags:
135+
return
136+
137+
add_set = item.tags - item._initial_tags
138+
remove_set = item._initial_tags - item.tags
139+
self.delete_tags(item, remove_set)
140+
if add_set:
141+
item.tags = self.add_tags(item, add_set)
142+
item._initial_tags = copy.copy(item.tags)
143+
logger.info(f"Updated tags to {item.tags}")
144+
145+
146+
content = Iterable[Union["ColumnItem", "DatabaseItem", "DatasourceItem", "FlowItem", "TableItem", "WorkbookItem"]]
147+
148+
149+
class Tags(Endpoint):
150+
def __init__(self, parent_srv: "Server"):
151+
super().__init__(parent_srv)
152+
153+
@property
154+
def baseurl(self):
155+
return f"{self.parent_srv.baseurl}/tags"
156+
157+
@api(version="3.9")
158+
def batch_add(self, tags: Union[Iterable[str], str], content: content) -> Set[str]:
159+
if isinstance(tags, str):
160+
tag_set = set([tags])
161+
else:
162+
tag_set = set(tags)
163+
164+
url = f"{self.baseurl}:batchCreate"
165+
batch_create_req = RequestFactory.Tag.batch_create(tag_set, content)
166+
server_response = self.put_request(url, batch_create_req)
167+
return TagItem.from_response(server_response.content, self.parent_srv.namespace)
168+
169+
@api(version="3.9")
170+
def batch_delete(self, tags: Union[Iterable[str], str], content: content) -> Set[str]:
171+
if isinstance(tags, str):
172+
tag_set = set([tags])
173+
else:
174+
tag_set = set(tags)
175+
176+
url = f"{self.baseurl}:batchDelete"
177+
# The batch delete XML is the same as the batch create XML.
178+
batch_delete_req = RequestFactory.Tag.batch_create(tag_set, content)
179+
server_response = self.put_request(url, batch_delete_req)
180+
return TagItem.from_response(server_response.content, self.parent_srv.namespace)

tableauserverclient/server/endpoint/tables_endpoint.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import logging
2+
from typing import Iterable, Set, Union
23

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
79
from tableauserverclient.server import RequestFactory
810
from tableauserverclient.models import TableItem, ColumnItem, PaginationItem
9-
from ..pager import Pager
11+
from tableauserverclient.server.pager import Pager
1012

1113
from tableauserverclient.helpers.logging import logger
1214

1315

14-
class Tables(Endpoint):
16+
class Tables(Endpoint, TaggingMixin):
1517
def __init__(self, parent_srv):
1618
super(Tables, self).__init__(parent_srv)
1719

@@ -124,3 +126,14 @@ def add_dqw(self, item, warning):
124126
@api(version="3.5")
125127
def delete_dqw(self, item):
126128
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")

tableauserverclient/server/endpoint/views_endpoint.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import logging
22
from contextlib import closing
33

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
48
from tableauserverclient.server.query import QuerySet
59

6-
from .endpoint import QuerysetEndpoint, api
7-
from .exceptions import MissingRequiredFieldError
8-
from .permissions_endpoint import _PermissionsEndpoint
9-
from .resource_tagger import _ResourceTagger
1010
from tableauserverclient.models import ViewItem, PaginationItem
1111

1212
from tableauserverclient.helpers.logging import logger
1313

14-
from typing import Iterator, List, Optional, Tuple, TYPE_CHECKING
14+
from typing import Iterable, Iterator, List, Optional, Set, Tuple, TYPE_CHECKING, Union
1515

1616
if TYPE_CHECKING:
17-
from ..request_options import (
17+
from tableauserverclient.server.request_options import (
1818
RequestOptions,
1919
CSVRequestOptions,
2020
PDFRequestOptions,
@@ -23,10 +23,9 @@
2323
)
2424

2525

26-
class Views(QuerysetEndpoint[ViewItem]):
26+
class Views(QuerysetEndpoint[ViewItem], TaggingMixin):
2727
def __init__(self, parent_srv):
2828
super(Views, self).__init__(parent_srv)
29-
self._resource_tagger = _ResourceTagger(parent_srv)
3029
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
3130

3231
# Used because populate_preview_image functionaliy requires workbook endpoint
@@ -171,11 +170,23 @@ def update(self, view_item: ViewItem) -> ViewItem:
171170
error = "View item missing ID. View must be retrieved from server first."
172171
raise MissingRequiredFieldError(error)
173172

174-
self._resource_tagger.update_tags(self.baseurl, view_item)
173+
self.update_tags(view_item)
175174

176175
# Returning view item to stay consistent with datasource/view update functions
177176
return view_item
178177

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+
179190
def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[ViewItem]:
180191
"""
181192
Queries the Tableau Server for items using the specified filters. Page

0 commit comments

Comments
 (0)
0