Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions tableauserverclient/server/endpoint/datasources_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
PathOrFileW = Union[FilePath, FileObjectW]


class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin):
class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]):
def __init__(self, parent_srv: "Server") -> None:
super(Datasources, self).__init__(parent_srv)
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
Expand Down Expand Up @@ -126,7 +126,7 @@ def download(
datasource_id: str,
filepath: Optional[PathOrFileW] = None,
include_extract: bool = True,
) -> str:
) -> PathOrFileW:
return self.download_revision(
datasource_id,
None,
Expand Down Expand Up @@ -405,7 +405,7 @@ def _get_datasource_revisions(
def download_revision(
self,
datasource_id: str,
revision_number: str,
revision_number: Optional[str],
filepath: Optional[PathOrFileW] = None,
include_extract: bool = True,
) -> PathOrFileW:
Expand Down
42 changes: 29 additions & 13 deletions tableauserverclient/server/endpoint/endpoint.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,41 @@
from typing_extensions import Concatenate, ParamSpec
from tableauserverclient import datetime_helpers as datetime

import abc
from packaging.version import Version
from functools import wraps
from xml.etree.ElementTree import ParseError
from typing import Any, Callable, Dict, Generic, List, Optional, TYPE_CHECKING, Tuple, TypeVar, Union
from typing import (
Any,
Callable,
Dict,
Generic,
List,
Optional,
TYPE_CHECKING,
Tuple,
TypeVar,
Union,
)

from tableauserverclient.models.pagination_item import PaginationItem
from tableauserverclient.server.request_options import RequestOptions

from .exceptions import (
from tableauserverclient.server.endpoint.exceptions import (
ServerResponseError,
InternalServerError,
NonXMLResponseError,
NotSignedInError,
)
from ..exceptions import EndpointUnavailableError
from tableauserverclient.server.exceptions import EndpointUnavailableError

from tableauserverclient.server.query import QuerySet
from tableauserverclient import helpers, get_versions

from tableauserverclient.helpers.logging import logger
from tableauserverclient.config import DELAY_SLEEP_SECONDS

if TYPE_CHECKING:
from ..server import Server
from tableauserverclient.server.server import Server
from requests import Response


Expand All @@ -38,7 +49,7 @@
USER_AGENT_HEADER = "User-Agent"


class Endpoint(object):
class Endpoint:
def __init__(self, parent_srv: "Server"):
self.parent_srv = parent_srv

Expand Down Expand Up @@ -232,7 +243,12 @@ def patch_request(self, url, xml_request, content_type=XML_CONTENT_TYPE, paramet
)


def api(version):
E = TypeVar("E", bound="Endpoint")
P = ParamSpec("P")
R = TypeVar("R")


def api(version: str) -> Callable[[Callable[Concatenate[E, P], R]], Callable[Concatenate[E, P], R]]:
"""Annotate the minimum supported version for an endpoint.

Checks the version on the server object and compares normalized versions.
Expand All @@ -251,9 +267,9 @@ def api(version):
>>> ...
"""

def _decorator(func):
def _decorator(func: Callable[Concatenate[E, P], R]) -> Callable[Concatenate[E, P], R]:
@wraps(func)
def wrapper(self, *args, **kwargs):
def wrapper(self: E, *args: P.args, **kwargs: P.kwargs) -> R:
self.parent_srv.assert_at_least_version(version, self.__class__.__name__)
return func(self, *args, **kwargs)

Expand All @@ -262,7 +278,7 @@ def wrapper(self, *args, **kwargs):
return _decorator


def parameter_added_in(**params):
def parameter_added_in(**params: str) -> Callable[[Callable[Concatenate[E, P], R]], Callable[Concatenate[E, P], R]]:
"""Annotate minimum versions for new parameters or request options on an endpoint.

The api decorator documents when an endpoint was added, this decorator annotates
Expand All @@ -285,9 +301,9 @@ def parameter_added_in(**params):
>>> ...
"""

def _decorator(func):
def _decorator(func: Callable[Concatenate[E, P], R]) -> Callable[Concatenate[E, P], R]:
@wraps(func)
def wrapper(self, *args, **kwargs):
def wrapper(self: E, *args: P.args, **kwargs: P.kwargs) -> R:
import warnings

server_ver = Version(self.parent_srv.version or "0.0")
Expand Down Expand Up @@ -335,5 +351,5 @@ def paginate(self, **kwargs) -> QuerySet[T]:
return queryset

@abc.abstractmethod
def get(self, request_options: RequestOptions) -> Tuple[List[T], PaginationItem]:
def get(self, request_options: Optional[RequestOptions] = None) -> Tuple[List[T], PaginationItem]:
raise NotImplementedError(f".get has not been implemented for {self.__class__.__qualname__}")
2 changes: 1 addition & 1 deletion tableauserverclient/server/endpoint/flows_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
PathOrFileW = Union[FilePath, FileObjectW]


class Flows(QuerysetEndpoint[FlowItem], TaggingMixin):
class Flows(QuerysetEndpoint[FlowItem], TaggingMixin[FlowItem]):
def __init__(self, parent_srv):
super(Flows, self).__init__(parent_srv)
self._resource_tagger = _ResourceTagger(parent_srv)
Expand Down
21 changes: 16 additions & 5 deletions tableauserverclient/server/endpoint/jobs_endpoint.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing_extensions import Self, overload

from tableauserverclient.server.query import QuerySet

Expand All @@ -13,15 +14,25 @@
from typing import List, Optional, Tuple, Union


class Jobs(QuerysetEndpoint[JobItem]):
class Jobs(QuerysetEndpoint[BackgroundJobItem]):
@property
def baseurl(self):
return "{0}/sites/{1}/jobs".format(self.parent_srv.baseurl, self.parent_srv.site_id)

@overload # type: ignore[override]
def get(self: Self, job_id: str, req_options: Optional[RequestOptionsBase] = None) -> JobItem: # type: ignore[override]
...

@overload # type: ignore[override]
def get(self: Self, job_id: RequestOptionsBase, req_options: None) -> Tuple[List[BackgroundJobItem], PaginationItem]: # type: ignore[override]
...

@overload # type: ignore[override]
def get(self: Self, job_id: None, req_options: Optional[RequestOptionsBase]) -> Tuple[List[BackgroundJobItem], PaginationItem]: # type: ignore[override]
...

@api(version="2.6")
def get(
self, job_id: Optional[str] = None, req_options: Optional[RequestOptionsBase] = None
) -> Tuple[List[BackgroundJobItem], PaginationItem]:
def get(self, job_id=None, req_options=None):
# Backwards Compatibility fix until we rev the major version
if job_id is not None and isinstance(job_id, str):
import warnings
Expand Down Expand Up @@ -77,7 +88,7 @@ def wait_for_job(self, job_id: Union[str, JobItem], *, timeout: Optional[float]
else:
raise AssertionError("Unexpected finish_code in job", job)

def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[JobItem]:
def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[BackgroundJobItem]:
"""
Queries the Tableau Server for items using the specified filters. Page
size can be specified to limit the number of items returned in a single
Expand Down
39 changes: 21 additions & 18 deletions tableauserverclient/server/endpoint/resource_tagger.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import abc
import copy
from typing import Iterable, Optional, Protocol, Set, Union, TYPE_CHECKING, runtime_checkable
from typing import Generic, Iterable, Optional, Protocol, Set, TypeVar, Union, TYPE_CHECKING, runtime_checkable
import urllib.parse

from tableauserverclient.server.endpoint.endpoint import Endpoint, api
Expand Down Expand Up @@ -62,27 +62,24 @@ def update_tags(self, baseurl, resource_item):
logger.info("Updated tags to {0}".format(resource_item.tags))


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


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

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


class Response(Protocol):
content: bytes
T = TypeVar("T")


class TaggingMixin(abc.ABC):
class TaggingMixin(abc.ABC, Generic[T]):
parent_srv: "Server"

@property
Expand All @@ -98,7 +95,7 @@ def put_request(self, url, request) -> Response:
def delete_request(self, url) -> None:
pass

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

if not isinstance(item_id, str):
Expand All @@ -114,7 +111,7 @@ def add_tags(self, item: Union[HasID, Taggable, str], tags: Union[Iterable[str],
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:
def delete_tags(self, item: Union[T, str], tags: Union[Iterable[str], str]) -> None:
item_id = getattr(item, "id", item)

if not isinstance(item_id, str):
Expand All @@ -130,17 +127,23 @@ def delete_tags(self, item: Union[HasID, Taggable, str], tags: Union[Iterable[st
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:
def update_tags(self, item: T) -> None:
if (initial_tags := getattr(item, "_initial_tags", None)) is None:
raise ValueError(f"{item} does not have initial tags.")
if (tags := getattr(item, "tags", None)) is None:
raise ValueError(f"{item} does not have tags.")
if tags == initial_tags:
return

add_set = item.tags - item._initial_tags
remove_set = item._initial_tags - item.tags
add_set = tags - initial_tags
remove_set = initial_tags - 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}")
tags = self.add_tags(item, add_set)
setattr(item, "tags", tags)

setattr(item, "_initial_tags", copy.copy(tags))
logger.info(f"Updated tags to {tags}")


content = Iterable[Union["ColumnItem", "DatabaseItem", "DatasourceItem", "FlowItem", "TableItem", "WorkbookItem"]]
Expand Down
2 changes: 1 addition & 1 deletion tableauserverclient/server/endpoint/tables_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from tableauserverclient.helpers.logging import logger


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

Expand Down
2 changes: 1 addition & 1 deletion tableauserverclient/server/endpoint/views_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
)


class Views(QuerysetEndpoint[ViewItem], TaggingMixin):
class Views(QuerysetEndpoint[ViewItem], TaggingMixin[ViewItem]):
def __init__(self, parent_srv):
super(Views, self).__init__(parent_srv)
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
Expand Down
4 changes: 2 additions & 2 deletions tableauserverclient/server/endpoint/workbooks_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
PathOrFileW = Union[FilePath, FileObjectW]


class Workbooks(QuerysetEndpoint[WorkbookItem], TaggingMixin):
class Workbooks(QuerysetEndpoint[WorkbookItem], TaggingMixin[WorkbookItem]):
def __init__(self, parent_srv: "Server") -> None:
super(Workbooks, self).__init__(parent_srv)
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
Expand Down Expand Up @@ -184,7 +184,7 @@ def download(
workbook_id: str,
filepath: Optional[PathOrFileW] = None,
include_extract: bool = True,
) -> str:
) -> PathOrFileW:
return self.download_revision(
workbook_id,
None,
Expand Down
Loading