8000 Merge pull request #1434 from jorwoods/jorwoods/custom_views_dl_pub · tableau/server-client-python@8d8adbd · GitHub
[go: up one dir, main page]

Skip to content

Commit 8d8adbd

Browse files
authored
Merge pull request #1434 from jorwoods/jorwoods/custom_views_dl_pub
feat: custom views dl pub
2 parents 3a53d00 + 17bd73a commit 8d8adbd

File tree

4 files changed

+272
-6
lines changed

4 files changed

+272
-6
lines changed

tableauserverclient/server/endpoint/custom_views_endpoint.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import io
12
import logging
2-
from typing import List, Optional, Tuple
3-
4-
from .endpoint import QuerysetEndpoint, api
5-
from .exceptions import MissingRequiredFieldError
3+
import os
4+
from pathlib import Path
5+
from typing import List, Optional, Tuple, Union
6+
7+
from tableauserverclient.config import BYTES_PER_MB, FILESIZE_LIMIT_MB
8+
from tableauserverclient.filesys_helpers import get_file_object_size
9+
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
10+
from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
611
from tableauserverclient.models import CustomViewItem, PaginationItem
712
from tableauserverclient.server import RequestFactory, RequestOptions, ImageRequestOptions
813

@@ -16,6 +21,15 @@
1621
update the name or owner of a custom view.
1722
"""
1823

24+
FilePath = Union[str, os.PathLike]
25+
FileObject = Union[io.BufferedReader, io.BytesIO]
26+
FileObjectR = Union[io.BufferedReader, io.BytesIO]
27+
FileObjectW = Union[io.BufferedWriter, io.BytesIO]
28+
PathOrFileR = Union[FilePath, FileObjectR]
29+
PathOrFileW = Union[FilePath, FileObjectW]
30+
io_types_r = (io.BufferedReader, io.BytesIO)
31+
io_types_w = (io.BufferedWriter, io.BytesIO)
32+
1933

2034
class CustomViews(QuerysetEndpoint[CustomViewItem]):
2135
def __init__(self, parent_srv):
@@ -25,6 +39,10 @@ def __init__(self, parent_srv):
2539
def baseurl(self) -> str:
2640
return "{0}/sites/{1}/customviews".format(self.parent_srv.baseurl, self.parent_srv.site_id)
2741

42+
@property
43+
def expurl(self) -> str:
44+
return f"{self.parent_srv._server_address}/api/exp/sites/{self.parent_srv.site_id}/customviews"
45+
2846
"""
2947
If the request has no filter parameters: Administrators will see all custom views.
3048
Other users will see only custom views that they own.
@@ -102,3 +120,46 @@ def delete(self, view_id: str) -> None:
102120
url = "{0}/{1}".format(self.baseurl, view_id)
103121
self.delete_request(url)
104122
logger.info("Deleted single custom view (ID: {0})".format(view_id))
123+
124+
@api(version="3.21")
125+
def download(self, view_item: CustomViewItem, file: PathOrFileW) -> PathOrFileW:
126+
url = f"{self.expurl}/{view_item.id}/content"
127+
server_response = self.get_request(url)
128+
if isinstance(file, io_types_w):
129+
file.write(server_response.content)
130+
return file
131+
132+
with open(file, "wb") as f:
133+
f.write(server_response.content)
134+
135+
return file
136+
137+
@api(version="3.21")
138+
def publish(self, view_item: CustomViewItem, file: PathOrFileR) -> Optional[CustomViewItem]:
139+
url = self.expurl
140+
if isinstance(file, io_types_r):
141+
size = get_file_object_size(file)
142+
elif isinstance(file, (str, Path)) and (p := Path(file)).is_file():
143+
size = p.stat().st_size
144+
else:
145+
raise ValueError("File path or file object required for publishing custom view.")
146+
147+
if size >= FILESIZE_LIMIT_MB * BYTES_PER_MB:
148+
upload_session_id = self.parent_srv.fileuploads.upload(file)
149+
url = f"{url}?uploadSessionId={upload_session_id}"
150+
xml_request, content_type = RequestFactory.CustomView.publish_req_chunked(view_item)
151+
else:
152+
if isinstance(file, io_types_r):
153+
file.seek(0)
154+
contents = file.read()
155+
if view_item.name is None:
156+
raise MissingRequiredFieldError("Custom view item missing name.")
157+
filename = view_item.name
158+
elif isinstance(file, (str, Path)):
159+
filename = Path(file).name
160+
contents = Path(file).read_bytes()
161+
162+
xml_request, content_type = RequestFactory.CustomView.publish_req(view_item, filename, contents)
163+
164+
server_response = self.post_request(url, xml_request, content_type)
165+
return CustomViewItem.from_response(server_response.content, self.parent_srv.namespace)

tableauserverclient/server/request_factory.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import xml.etree.ElementTree as ET
2+
23
from typing import Any, Dict, Iterable, List, Optional, Tuple, TYPE_CHECKING, Union
34

45
from requests.packages.urllib3.fields import RequestField
@@ -1267,6 +1268,49 @@ def update_req(self, xml_request: ET.Element, custom_view_item: CustomViewItem):
12671268
if custom_view_item.name is not None:
12681269
updating_element.attrib["name"] = custom_view_item.name
12691270

1271+
@_tsrequest_wrapped
1272+
def _publish_xml(self, xml_request: ET.Element, custom_view_item: CustomViewItem) -> bytes:
1273+
custom_view_element = ET.SubElement(xml_request, "customView")
1274+
if (name := custom_view_item.name) is not None:
1275+
custom_view_element.attrib["name"] = name
1276+
else:
1277+
raise ValueError(f"Custom View Item missing name: {custom_view_item}")
1278+
if (shared := custom_view_item.shared) is not None:
1279+
custom_view_element.attrib["shared"] = str(shared).lower()
1280+
else:
1281+
raise ValueError(f"Custom View Item missing shared: {custom_view_item}")
1282+
if (owner := custom_view_item.owner) is not None:
1283+
owner_element = ET.SubElement(custom_view_element, "owner")
1284+
if (owner_id := owner.id) is not None:
1285+
owner_element.attrib["id"] = owner_id
1286+
else:
1287+
raise ValueError(f"Custom View Item owner missing id: {owner}")
1288+
else:
1289+
raise ValueError(f"Custom View Item missing owner: {custom_view_item}")
1290+
if (workbook := custom_view_item.workbook) is not None:
1291+
workbook_element = ET.SubElement(custom_view_element, "workbook")
1292+
if (workbook_id := workbook.id) is not None:
1293+
workbook_element.attrib["id"] = workbook_id
1294+
else:
1295+
raise ValueError(f"Custom View Item workbook missing id: {workbook}")
1296+
else:
1297+
raise ValueError(f"Custom View Item missing workbook: {custom_view_item}")
1298+
1299+
return ET.tostring(xml_request)
1300+
1301+
def publish_req_chunked(self, custom_view_item: CustomViewItem):
1302+
xml_request = self._publish_xml(custom_view_item)
1303+
parts = {"request_payload": ("", xml_request, "text/xml")}
1304+
return _add_multipart(parts)
1305+
1306+
def publish_req(self, custom_view_item: CustomViewItem, filename: str, file_contents: bytes):
1307+
xml_request = self._publish_xml(custom_view_item)
1308+
parts = {
1309+
"request_payload": ("", xml_request, "text/xml"),
1310+
"tableau_customview": (filename, file_contents, "application/octet-stream"),
1311+
}
1312+
return _add_multipart(parts)
1313+
12701314

12711315
class GroupSetRequest:
12721316
@_tsrequest_wrapped

0 commit comments

Comments
 (0)
0