forked from tableau/server-client-python
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request tableau#1429 from jorwoods/jorwoods/virtual_connections
- Loading branch information
Showing
19 changed files
with
664 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import datetime as dt | ||
import json | ||
from typing import Callable, Dict, Iterable, List, Optional | ||
from xml.etree.ElementTree import Element | ||
|
||
from defusedxml.ElementTree import fromstring | ||
|
||
from tableauserverclient.datetime_helpers import parse_datetime | ||
from tableauserverclient.models.connection_item import ConnectionItem | ||
from tableauserverclient.models.exceptions import UnpopulatedPropertyError | ||
from tableauserverclient.models.permissions_item import PermissionsRule | ||
|
||
|
||
class VirtualConnectionItem: | ||
def __init__(self, name: str) -> None: | ||
self.name = name | ||
self.created_at: Optional[dt.datetime] = None | ||
self.has_extracts: Optional[bool] = None | ||
self._id: Optional[str] = None | ||
self.is_certified: Optional[bool] = None | ||
self.updated_at: Optional[dt.datetime] = None | ||
self.webpage_url: Optional[str] = None | ||
self._connections: Optional[Callable[[], Iterable[ConnectionItem]]] = None | ||
self.project_id: Optional[str] = None | ||
self.owner_id: Optional[str] = None | ||
self.content: Optional[Dict[str, dict]] = None | ||
self.certification_note: Optional[str] = None | ||
|
||
def __str__(self) -> str: | ||
return f"{self.__class__.__qualname__}(name={self.name})" | ||
|
||
def __repr__(self) -> str: | ||
return f"<{self!s}>" | ||
|
||
def _set_permissions(self, permissions): | ||
self._permissions = permissions | ||
|
||
@property | ||
def id(self) -> Optional[str]: | ||
return self._id | ||
|
||
@property | ||
def permissions(self) -> List[PermissionsRule]: | ||
if self._permissions is None: | ||
error = "Workbook item must be populated with permissions first." | ||
raise UnpopulatedPropertyError(error) | ||
return self._permissions() | ||
|
||
@property | ||
def connections(self) -> Iterable[ConnectionItem]: | ||
if self._connections is None: | ||
raise AttributeError("connections not populated. Call populate_connections() first.") | ||
return self._connections() | ||
|
||
@classmethod | ||
def from_response(cls, response: bytes, ns: Dict[str, str]) -> List["VirtualConnectionItem"]: | ||
parsed_response = fromstring(response) | ||
return [cls.from_xml(xml, ns) for xml in parsed_response.findall(".//t:virtualConnection[@name]", ns)] | ||
|
||
@classmethod | ||
def from_xml(cls, xml: Element, ns: Dict[str, str]) -> "VirtualConnectionItem": | ||
v_conn = cls(xml.get("name", "")) | ||
v_conn._id = xml.get("id", None) | ||
v_conn.webpage_url = xml.get("webpageUrl", None) | ||
v_conn.created_at = parse_datetime(xml.get("createdAt", None)) | ||
v_conn.updated_at = parse_datetime(xml.get("updatedAt", None)) | ||
v_conn.is_certified = string_to_bool(s) if (s := xml.get("isCertified", None)) else None | ||
v_conn.certification_note = xml.get("certificationNote", None) | ||
v_conn.has_extracts = string_to_bool(s) if (s := xml.get("hasExtracts", None)) else None | ||
v_conn.project_id = p.get("id", None) if ((p := xml.find(".//t:project[@id]", ns)) is not None) else None | ||
v_conn.owner_id = o.get("id", None) if ((o := xml.find(".//t:owner[@id]", ns)) is not None) else None | ||
v_conn.content = json.loads(c.text or "{}") if ((c := xml.find(".//t:content", ns)) is not None) else None | ||
return v_conn | ||
|
||
|
||
def string_to_bool(s: str) -> bool: | ||
return s.lower() in ["true", "1", "t", "y", "yes"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
173 changes: 173 additions & 0 deletions
173
tableauserverclient/server/endpoint/virtual_connections_endpoint.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
from functools import partial | ||
import json | ||
from pathlib import Path | ||
from typing import Iterable, List, Optional, Set, TYPE_CHECKING, Tuple, Union | ||
|
||
from tableauserverclient.models.connection_item import ConnectionItem | ||
from tableauserverclient.models.pagination_item import PaginationItem | ||
from tableauserverclient.models.revision_item import RevisionItem | ||
from tableauserverclient.models.virtual_connection_item import VirtualConnectionItem | ||
from tableauserverclient.server.request_factory import RequestFactory | ||
from tableauserverclient.server.request_options import RequestOptions | ||
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api | ||
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint | ||
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin | ||
from tableauserverclient.server.pager import Pager | ||
|
||
if TYPE_CHECKING: | ||
from tableauserverclient.server import Server | ||
|
||
|
||
class VirtualConnections(QuerysetEndpoint[VirtualConnectionItem], TaggingMixin): | ||
def __init__(self, parent_srv: "Server") -> None: | ||
super().__init__(parent_srv) | ||
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl) | ||
|
||
@property | ||
def baseurl(self) -> str: | ||
return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/virtualConnections" | ||
|
||
@api(version="3.18") | ||
def get(self, req_options: Optional[RequestOptions] = None) -> Tuple[List[VirtualConnectionItem], PaginationItem]: | ||
server_response = self.get_request(self.baseurl, req_options) | ||
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) | ||
virtual_connections = VirtualConnectionItem.from_response(server_response.content, self.parent_srv.namespace) | ||
return virtual_connections, pagination_item | ||
|
||
@api(version="3.18") | ||
def populate_connections(self, virtual_connection: VirtualConnectionItem) -> VirtualConnectionItem: | ||
def _connection_fetcher(): | ||
return Pager(partial(self._get_virtual_database_connections, virtual_connection)) | ||
|
||
virtual_connection._connections = _connection_fetcher | ||
return virtual_connection | ||
|
||
def _get_virtual_database_connections( | ||
self, virtual_connection: VirtualConnectionItem, req_options: Optional[RequestOptions] = None | ||
) -> Tuple[List[ConnectionItem], PaginationItem]: | ||
server_response = self.get_request(f"{self.baseurl}/{virtual_connection.id}/connections", req_options) | ||
connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace) | ||
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) | ||
|
||
return connections, pagination_item | ||
|
||
@api(version="3.18") | ||
def update_connection_db_connection( | ||
self, virtual_connection: Union[str, VirtualConnectionItem], connection: ConnectionItem | ||
) -> ConnectionItem: | ||
vconn_id = getattr(virtual_connection, "id", virtual_connection) | ||
url = f"{self.baseurl}/{vconn_id}/connections/{connection.id}/modify" | ||
xml_request = RequestFactory.VirtualConnection.update_db_connection(connection) | ||
server_response = self.put_request(url, xml_request) | ||
return ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0] | ||
|
||
@api(version="3.23") | ||
def get_by_id(self, virtual_connection: Union[str, VirtualConnectionItem]) -> VirtualConnectionItem: | ||
vconn_id = getattr(virtual_connection, "id", virtual_connection) | ||
url = f"{self.baseurl}/{vconn_id}" | ||
server_response = self.get_request(url) | ||
return VirtualConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0] | ||
|
||
@api(version="3.23") | ||
def download(self, virtual_connection: Union[str, VirtualConnectionItem]) -> str: | ||
v_conn = self.get_by_id(virtual_connection) | ||
return json.dumps(v_conn.content) | ||
|
||
@api(version="3.23") | ||
def update(self, virtual_connection: VirtualConnectionItem) -> VirtualConnectionItem: | ||
url = f"{self.baseurl}/{virtual_connection.id}" | ||
xml_request = RequestFactory.VirtualConnection.update(virtual_connection) | ||
server_response = self.put_request(url, xml_request) | ||
return VirtualConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0] | ||
|
||
@api(version="3.23") | ||
def get_revisions( | ||
self, virtual_connection: VirtualConnectionItem, req_options: Optional[RequestOptions] = None | ||
) -> Tuple[List[RevisionItem], PaginationItem]: | ||
server_response = self.get_request(f"{self.baseurl}/{virtual_connection.id}/revisions", req_options) | ||
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) | ||
revisions = RevisionItem.from_response(server_response.content, self.parent_srv.namespace, virtual_connection) | ||
return revisions, pagination_item | ||
|
||
@api(version="3.23") | ||
def download_revision(self, virtual_connection: VirtualConnectionItem, revision_number: int) -> str: | ||
url = f"{self.baseurl}/{virtual_connection.id}/revisions/{revision_number}" | ||
server_response = self.get_request(url) | ||
virtual_connection = VirtualConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0] | ||
return json.dumps(virtual_connection.content) | ||
|
||
@api(version="3.23") | ||
def delete(self, virtual_connection: Union[VirtualConnectionItem, str]) -> None: | ||
vconn_id = getattr(virtual_connection, "id", virtual_connection) | ||
self.delete_request(f"{self.baseurl}/{vconn_id}") | ||
|
||
@api(version="3.23") | ||
def publish( | ||
self, | ||
virtual_connection: VirtualConnectionItem, | ||
virtual_connection_content: str, | ||
mode: str = "CreateNew", | ||
publish_as_draft: bool = False, | ||
) -> VirtualConnectionItem: | ||
""" | ||
Publish a virtual connection to the server. | ||
For the virtual_connection object, name, project_id, and owner_id are | ||
required. | ||
The virtual_connection_content can be a json string or a file path to a | ||
json file. | ||
The mode can be "CreateNew" or "Overwrite". If mode is | ||
"Overwrite" and the virtual connection already exists, it will be | ||
overwritten. | ||
If publish_as_draft is True, the virtual connection will be published | ||
as a draft, and the id of the draft will be on the response object. | ||
""" | ||
try: | ||
json.loads(virtual_connection_content) | ||
except json.JSONDecodeError: | ||
file = Path(virtual_connection_content) | ||
if not file.exists(): | ||
raise RuntimeError(f"{virtual_connection_content} is not valid json nor an existing file path") | ||
content = file.read_text() | ||
else: | ||
content = virtual_connection_content | ||
|
||
if mode not in ["CreateNew", "Overwrite"]: | ||
raise ValueError(f"Invalid mode: {mode}") | ||
overwrite = mode == "Overwrite" | ||
|
||
url = f"{self.baseurl}?overwrite={str(overwrite).lower()}&publishAsDraft={str(publish_as_draft).lower()}" | ||
xml_request = RequestFactory.VirtualConnection.publish(virtual_connection, content) | ||
server_response = self.post_request(url, xml_request) | ||
return VirtualConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0] | ||
|
||
@api(version="3.22") | ||
def populate_permissions(self, item: VirtualConnectionItem) -> None: | ||
self._permissions.populate(item) | ||
|
||
@api(version="3.22") | ||
def add_permissions(self, resource, rules): | ||
return self._permissions.update(resource, rules) | ||
|
||
@api(version="3.22") | ||
def delete_permission(self, item, capability_item): | ||
return self._permissions.delete(item, capability_item) | ||
|
||
@api(version="3.23") | ||
def add_tags( | ||
self, virtual_connection: Union[VirtualConnectionItem, str], tags: Union[Iterable[str], str] | ||
) -> Set[str]: | ||
return super().add_tags(virtual_connection, tags) | ||
|
||
@api(version="3.23") | ||
def delete_tags( | ||
self, virtual_connection: Union[VirtualConnectionItem, str], tags: Union[Iterable[str], str] | ||
) -> None: | ||
return super().delete_tags(virtual_connection, tags) | ||
|
||
@api(version="3.23") | ||
def update_tags(self, virtual_connection: VirtualConnectionItem) -> None: | ||
raise NotImplementedError("Update tags is not implemented for Virtual Connections") |
Oops, something went wrong.