Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions tableauserverclient/server/endpoint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from tableauserverclient.server.endpoint.sites_endpoint import Sites
from tableauserverclient.server.endpoint.subscriptions_endpoint import Subscriptions
from tableauserverclient.server.endpoint.tables_endpoint import Tables
from tableauserverclient.server.endpoint.resource_tagger import Tags
from tableauserverclient.server.endpoint.tasks_endpoint import Tasks
from tableauserverclient.server.endpoint.users_endpoint import Users
from tableauserverclient.server.endpoint.views_endpoint import Views
Expand Down Expand Up @@ -55,6 +56,7 @@
"Sites",
"Subscriptions",
"Tables",
"Tags",
"Tasks",
"Users",
"Views",
Expand Down
28 changes: 21 additions & 7 deletions tableauserverclient/server/endpoint/databases_endpoint.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import logging

from .default_permissions_endpoint import _DefaultPermissionsEndpoint
from .dqw_endpoint import _DataQualityWarningEndpoint
from .endpoint import api, Endpoint
from .exceptions import MissingRequiredFieldError
from .permissions_endpoint import _PermissionsEndpoint
from typing import Union, Iterable, Set

from tableauserverclient.server.endpoint.default_permissions_endpoint import _DefaultPermissionsEndpoint
from tableauserverclient.server.endpoint.dqw_endpoint import _DataQualityWarningEndpoint
from tableauserverclient.server.endpoint.endpoint import api, Endpoint
from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
from tableauserverclient.server import RequestFactory
from tableauserverclient.models import DatabaseItem, TableItem, PaginationItem, Resource

from tableauserverclient.helpers.logging import logger


class Databases(Endpoint):
class Databases(Endpoint, TaggingMixin):
def __init__(self, parent_srv):
super(Databases, self).__init__(parent_srv)

Expand Down Expand Up @@ -123,3 +125,15 @@ def add_dqw(self, item, warning):
@api(version="3.5")
def delete_dqw(self, item):
self._data_quality_warnings.clear(item)

@api(version="3.9")
def add_tags(self, item: Union[DatabaseItem, str], tags: Iterable[str]) -> Set[str]:
return super().add_tags(item, tags)

@api(version="3.9")
def delete_tags(self, item: Union[DatabaseItem, str], tags: Iterable[str]) -> None:
super().delete_tags(item, tags)

@api(version="3.9")
def update_tags(self, item: DatabaseItem) -> None:
raise NotImplementedError("Update tags is not supported for databases.")
21 changes: 16 additions & 5 deletions tableauserverclient/server/endpoint/datasources_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from contextlib import closing
from pathlib import Path
from typing import List, Mapping, Optional, Sequence, Tuple, TYPE_CHECKING, Union
from typing import Iterable, List, Mapping, Optional, Sequence, Set, Tuple, TYPE_CHECKING, Union

from tableauserverclient.helpers.headers import fix_filename
from tableauserverclient.server.query import QuerySet
Expand All @@ -20,7 +20,7 @@
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api, parameter_added_in
from tableauserverclient.server.endpoint.exceptions import InternalServerError, MissingRequiredFieldError
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
from tableauserverclient.server.endpoint.resource_tagger import _ResourceTagger
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin

from tableauserverclient.config import ALLOWED_FILE_EXTENSIONS, FILESIZE_LIMIT_MB, BYTES_PER_MB, CHUNK_SIZE_MB
from tableauserverclient.filesys_helpers import (
Expand Down Expand Up @@ -55,10 +55,9 @@
PathOrFileW = Union[FilePath, FileObjectW]


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

Expand Down Expand Up @@ -150,7 +149,7 @@ def update(self, datasource_item: DatasourceItem) -> DatasourceItem:
)
raise MissingRequiredFieldError(error)

self._resource_tagger.update_tags(self.baseurl, datasource_item)
self.update_tags(datasource_item)

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

@api(version="1.0")
def add_tags(self, item: Union[DatasourceItem, str], tags: Union[Iterable[str], str]) -> Set[str]:
return super().add_tags(item, tags)

@api(version="1.0")
def delete_tags(self, item: Union[DatasourceItem, str], tags: Union[Iterable[str], str]) -> None:
return super().delete_tags(item, tags)

@api(version="1.0")
def update_tags(self, item: DatasourceItem) -> None:
return super().update_tags(item)

def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[DatasourceItem]:
"""
Queries the Tableau Server for items using the specified filters. Page
Expand Down
4 changes: 2 additions & 2 deletions tableauserverclient/server/endpoint/flows_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
from tableauserverclient.server.endpoint.exceptions import InternalServerError, MissingRequiredFieldError
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
from tableauserverclient.server.endpoint.resource_tagger import _ResourceTagger
from tableauserverclient.server.endpoint.resource_tagger import _ResourceTagger, TaggingMixin
from tableauserverclient.models import FlowItem, PaginationItem, ConnectionItem, JobItem
from tableauserverclient.server import RequestFactory
from tableauserverclient.filesys_helpers import (
Expand Down Expand Up @@ -51,7 +51,7 @@
PathOrFileW = Union[FilePath, FileObjectW]


class Flows(QuerysetEndpoint[FlowItem]):
class Flows(QuerysetEndpoint[FlowItem], TaggingMixin):
def __init__(self, parent_srv):
super(Flows, self).__init__(parent_srv)
self._resource_tagger = _ResourceTagger(parent_srv)
Expand Down
135 changes: 132 additions & 3 deletions tableauserverclient/server/endpoint/resource_tagger.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import abc
import copy
from typing import Iterable, Optional, Protocol, Set, Union, TYPE_CHECKING, runtime_checkable
import urllib.parse

from .endpoint import Endpoint
from .exceptions import ServerResponseError
from ..exceptions import EndpointUnavailableError
from tableauserverclient.server.endpoint.endpoint import Endpoint, api
from tableauserverclient.server.endpoint.exceptions import ServerResponseError
from tableauserverclient.server.exceptions import EndpointUnavailableError
from tableauserverclient.server import RequestFactory
from tableauserverclient.models import TagItem

from tableauserverclient.helpers.logging import logger

if TYPE_CHECKING:
from tableauserverclient.models.column_item import ColumnItem
from tableauserverclient.models.database_item import DatabaseItem
from tableauserverclient.models.datasource_item import DatasourceItem
from tableauserverclient.models.flow_item import FlowItem
from tableauserverclient.models.table_item import TableItem
from tableauserverclient.models.workbook_item import WorkbookItem
from tableauserverclient.server.server import Server


class _ResourceTagger(Endpoint):
# Add new tags to resource
Expand Down Expand Up @@ -49,3 +60,121 @@ def update_tags(self, baseurl, resource_item):
resource_item.tags = self._add_tags(baseurl, resource_item.id, add_set)
resource_item._initial_tags = copy.copy(resource_item.tags)
logger.info("Updated tags to {0}".format(resource_item.tags))


class HasID(Protocol):
@property
def id(self) -> Optional[str]:
pass


@runtime_checkable
class Taggable(Protocol):
_initial_tags: Set[str]
tags: Set[str]

@property
def id(self) -> Optional[str]:
pass


class Response(Protocol):
content: bytes


class TaggingMixin(abc.ABC):
parent_srv: "Server"

@property
@abc.abstractmethod
def baseurl(self) -> str:
pass

@abc.abstractmethod
def put_request(self, url, request) -> Response:
pass

@abc.abstractmethod
def delete_request(self, url) -> None:
pass

def add_tags(self, item: Union[HasID, Taggable, str], tags: Union[Iterable[str], str]) -> Set[str]:
item_id = getattr(item, "id", item)

if not isinstance(item_id, str):
raise ValueError("ID not found.")

if isinstance(tags, str):
tag_set = set([tags])
else:
tag_set = set(tags)

url = f"{self.baseurl}/{item_id}/tags"
add_req = RequestFactory.Tag.add_req(tag_set)
server_response = self.put_request(url, add_req)
return TagItem.from_response(server_response.content, self.parent_srv.namespace)

def delete_tags(self, item: Union[HasID, Taggable, str], tags: Union[Iterable[str], str]) -> None:
item_id = getattr(item, "id", item)

if not isinstance(item_id, str):
raise ValueError("ID not found.")

if isinstance(tags, str):
tag_set = set([tags])
else:
tag_set = set(tags)

for tag in tag_set:
encoded_tag_name = urllib.parse.quote(tag)
url = f"{self.baseurl}/{item_id}/tags/{encoded_tag_name}"
self.delete_request(url)

def update_tags(self, item: Taggable) -> None:
if item.tags == item._initial_tags:
return

add_set = item.tags - item._initial_tags
remove_set = item._initial_tags - item.tags
self.delete_tags(item, remove_set)
if add_set:
item.tags = self.add_tags(item, add_set)
item._initial_tags = copy.copy(item.tags)
logger.info(f"Updated tags to {item.tags}")


content = Iterable[Union["ColumnItem", "DatabaseItem", "DatasourceItem", "FlowItem", "TableItem", "WorkbookItem"]]


class Tags(Endpoint):
def __init__(self, parent_srv: "Server"):
super().__init__(parent_srv)

@property
def baseurl(self):
return f"{self.parent_srv.baseurl}/tags"

@api(version="3.9")
def batch_add(self, tags: Union[Iterable[str], str], content: content) -> Set[str]:
if isinstance(tags, str):
tag_set = set([tags])
else:
tag_set = set(tags)

url = f"{self.baseurl}:batchCreate"
batch_create_req = RequestFactory.Tag.batch_create(tag_set, content)
server_response = self.put_request(url, batch_create_req)
return TagItem.from_response(server_response.content, self.parent_srv.namespace)

@api(version="3.9")
def batch_delete(self, tags: Union[Iterable[str], str], content: content) -> Set[str]:
if isinstance(tags, str):
tag_set = set([tags])
else:
tag_set = set(tags)

url = f"{self.baseurl}:batchDelete"
# The batch delete XML is the same as the batch create XML.
batch_delete_req = RequestFactory.Tag.batch_create(tag_set, content)
server_response = self.put_request(url, batch_delete_req)
return TagItem.from_response(server_response.content, self.parent_srv.namespace)
25 changes: 19 additions & 6 deletions tableauserverclient/server/endpoint/tables_endpoint.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import logging
from typing import Iterable, Set, Union

from .dqw_endpoint import _DataQualityWarningEndpoint
from .endpoint import api, Endpoint
from .exceptions import MissingRequiredFieldError
from .permissions_endpoint import _PermissionsEndpoint
from tableauserverclient.server.endpoint.dqw_endpoint import _DataQualityWarningEndpoint
from tableauserverclient.server.endpoint.endpoint import api, Endpoint
from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
from tableauserverclient.server import RequestFactory
from tableauserverclient.models import TableItem, ColumnItem, PaginationItem
from ..pager import Pager
from tableauserverclient.server.pager import Pager

from tableauserverclient.helpers.logging import logger


class Tables(Endpoint):
class Tables(Endpoint, TaggingMixin):
def __init__(self, parent_srv):
super(Tables, self).__init__(parent_srv)

Expand Down Expand Up @@ -124,3 +126,14 @@ def add_dqw(self, item, warning):
@api(version="3.5")
def delete_dqw(self, item):
self._data_quality_warnings.clear(item)

@api(version="3.9")
def add_tags(self, item: Union[TableItem, str], tags: Union[Iterable[str], str]) -> Set[str]:
return super().add_tags(item, tags)

@api(version="3.9")
def delete_tags(self, item: Union[TableItem, str], tags: Union[Iterable[str], str]) -> None:
return super().delete_tags(item, tags)

def update_tags(self, item: TableItem) -> None: # type: ignore
raise NotImplementedError("Update tags is not implemented for TableItem")
29 changes: 20 additions & 9 deletions tableauserverclient/server/endpoint/views_endpoint.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import logging
from contextlib import closing

from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
from tableauserverclient.server.query import QuerySet

from .endpoint import QuerysetEndpoint, api
from .exceptions import MissingRequiredFieldError
from .permissions_endpoint import _PermissionsEndpoint
from .resource_tagger import _ResourceTagger
from tableauserverclient.models import ViewItem, PaginationItem

from tableauserverclient.helpers.logging import logger

from typing import Iterator, List, Optional, Tuple, TYPE_CHECKING
from typing import Iterable, Iterator, List, Optional, Set, Tuple, TYPE_CHECKING, Union

if TYPE_CHECKING:
from ..request_options import (
from tableauserverclient.server.request_options import (
RequestOptions,
CSVRequestOptions,
PDFRequestOptions,
Expand All @@ -23,10 +23,9 @@
)


class Views(QuerysetEndpoint[ViewItem]):
class Views(QuerysetEndpoint[ViewItem], TaggingMixin):
def __init__(self, parent_srv):
super(Views, self).__init__(parent_srv)
self._resource_tagger = _ResourceTagger(parent_srv)
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)

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

self._resource_tagger.update_tags(self.baseurl, view_item)
self.update_tags(view_item)

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

@api(version="1.0")
def add_tags(self, item: Union[ViewItem, str], tags: Union[Iterable[str], str]) -> Set[str]:
return super().add_tags(item, tags)

@api(version="1.0")
def delete_tags(self, item: Union[ViewItem, str], tags: Union[Iterable[str], str]) -> None:
return super().delete_tags(item, tags)

@api(version="1.0")
def update_tags(self, item: ViewItem) -> None:
return super().update_tags(item)

def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[ViewItem]:
"""
Queries the Tableau Server for items using the specified filters. Page
Expand Down
Loading