8000 Expose the `fileuploads` API endpoint · gentleway/server-client-python@95bb0ca · GitHub
[go: up one dir, main page]

Skip to content

Commit 95bb0ca

Browse files
committed
Expose the fileuploads API endpoint
We had at least two independent re-implementations [1, 2] of file uploads within the last 4 months. And this was despite the fact that both projects already used TSC which would offer this functionality. Currently, the upload functionality in TSC is hard to discover as it is not exposed like all other REST functions. Instead of `server.fileuploads`, one has to first create an instance of the (undocumented) `Fileuploads` class. The upload functionality was probably because it should be usually unnecessary: The uploaded files are usually part of publishing a workbook/datasource/... and the corresponding `datasources.publish` (and similar) already take care of the upload internally. However, TSC isn't always up-to-date with new REST APIs, and by exposing file uploads directly we can make sure to offer the best possible experience to users of TSC also in those transition periods. This commit: * turns the `Fileuploads` class into a normal endpoint class which is not tied to one upload (So far, `Fileuploads` was not stateless. Now it is) * adds the endpoint to `server`, such that file uploads are available as `server.fileuploads` * adjusts all other users to use `server.fileuploads` instead of constructing an ad hoc instance of the `Fileuploads` class Documentation will be added in a separate commit. [1] https://github.com/jharris126/tableau-data-update-api-samples/blob/41f51ae4d220de55caf63e91fe9eff5694b9456a/basic/basic_incremental_load.py#L23 [2] https://github.com/tableau/hyper-api-samples/blob/382e66481ec8339407cf9cfa5d41fcdcf3f6a0fb/Community-Supported/clouddb-extractor/tableau_restapi_helpers.py#L165
1 parent d043e58 commit 95bb0ca

File tree

7 files changed

+24
-39
lines changed

7 files changed

+24
-39
lines changed

tableauserverclient/server/endpoint/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .databases_endpoint import Databases
66
from .endpoint import Endpoint
77
from .favorites_endpoint import Favorites
8+
from .fileuploads_endpoint import Fileuploads
89
from .flows_endpoint import Flows
910
from .exceptions import (
1011
ServerResponseError,

tableauserverclient/server/endpoint/datasources_endpoint.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from .exceptions import InternalServerError, MissingRequiredFieldError
33
from .permissions_endpoint import _PermissionsEndpoint
44
from .dqw_endpoint import _DataQualityWarningEndpoint
5-
from .fileuploads_endpoint import Fileuploads
65
from .resource_tagger import _ResourceTagger
76
from .. import RequestFactory, DatasourceItem, PaginationItem, ConnectionItem
87
from ..query import QuerySet
@@ -244,7 +243,7 @@ def publish(
244243
# Determine if chunking is required (64MB is the limit for single upload method)
245244
if file_size >= FILESIZE_LIMIT:
246245
logger.info("Publishing {0} to server with chunking method (datasource over 64MB)".format(filename))
247-
upload_session_id = Fileuploads.upload_chunks(self.parent_srv, file)
246+
upload_session_id = self.parent_srv.fileuploads.upload(file)
248247
url = "{0}&uploadSessionId={1}".format(url, upload_session_id)
249248
xml_request, content_type = RequestFactory.Datasource.publish_req_chunked(
250249
datasource_item, connection_credentials, connections

tableauserverclient/server/endpoint/fileuploads_endpoint.py

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
class Fileuploads(Endpoint):
1515
def __init__(self, parent_srv):
1616
super(Fileuploads, self).__init__(parent_srv)
17-
self.upload_id = ""
1817

1918
@property
2019
def baseurl(self):
@@ -25,21 +24,18 @@ def initiate(self):
2524
url = self.baseurl
2625
server_response = self.post_request(url, "")
2726
fileupload_item = FileuploadItem.from_response(server_response.content, self.parent_srv.namespace)
28-
self.upload_id = fileupload_item.upload_session_id
29-
logger.info("Initiated file upload session (ID: {0})".format(self.upload_id))
30-
return self.upload_id
27+
upload_id = fileupload_item.upload_session_id
28+
logger.info("Initiated file upload session (ID: {0})".format(upload_id))
29+
return upload_id
3130

3231
@api(version="2.0")
33-
def append(self, xml_request, content_type):
34-
if not self.upload_id:
35-
error = "File upload session must be initiated first."
36-
raise MissingRequiredFieldError(error)
37-
url = "{0}/{1}".format(self.baseurl, self.upload_id)
38-
server_response = self.put_request(url, xml_request, content_type)
39-
logger.info("Uploading a chunk to session (ID: {0})".format(self.upload_id))
32+
def append(self, upload_id, data, content_type):
33+
url = "{0}/{1}".format(self.baseurl, upload_id)
34+
server_response = self.put_request(url, data, content_type)
35+
logger.info("Uploading a chunk to session (ID: {0})".format(upload_id))
4036
return FileuploadItem.from_response(server_response.content, self.parent_srv.namespace)
4137

42-
def read_chunks(self, file):
38+
def _read_chunks(self, file):
4339
file_opened = False
4440
try:
4541
file_content = open(file, "rb")
@@ -55,15 +51,11 @@ def read_chunks(self, file):
5551
break
5652
yield chunked_content
5753

58-
@classmethod
59-
def upload_chunks(cls, parent_srv, file):
60-
file_uploader = cls(parent_srv)
61-
upload_id = file_uploader.initiate()
62-
63-
chunks = file_uploader.read_chunks(file)
64-
for chunk in chunks:
65-
xml_request, content_type = RequestFactory.Fileupload.chunk_req(chunk)
66-
fileupload_item = file_uploader.append(xml_request, content_type)
54+
def upload(self, fil 9E88 e):
55+
upload_id = self.initiate()
56+
for chunk in self._read_chunks(file):
57+
request, content_type = RequestFactory.Fileupload.chunk_req(chunk)
58+
fileupload_item = self.append(upload_id, request, content_type)
6759
logger.info("\tPublished {0}MB".format(fileupload_item.file_size))
68-
logger.info("\tCommitting file upload...")
60+
logger.info("File upload finished (ID: {0})".format(upload_id))
6961
return upload_id

tableauserverclient/server/endpoint/flows_endpoint.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from .exceptions import InternalServerError, MissingRequiredFieldError
33
from .permissions_endpoint import _PermissionsEndpoint
44
from .dqw_endpoint import _DataQualityWarningEndpoint
5-
from .fileuploads_endpoint import Fileuploads
65
from .resource_tagger import _ResourceTagger
76
from .. import RequestFactory, FlowItem, PaginationItem, ConnectionItem
87
from ...filesys_helpers import to_filename, make_download_path
@@ -169,7 +168,7 @@ def publish(self, flow_item, file_path, mode, connections=None):
169168
# Determine if chunking is required (64MB is the limit for single upload method)
170169
if os.path.getsize(file_path) >= FILESIZE_LIMIT:
171170
logger.info("Publishing {0} to server with chunking method (flow over 64MB)".format(filename))
172-
upload_session_id = Fileuploads.upload_chunks(self.parent_srv, file_path)
171+
upload_session_id = self.parent_srv.fileuploads.upload(file_path)
173172
url = "{0}&uploadSessionId={1}".format(url, upload_session_id)
174173
xml_request, content_type = RequestFactory.Flow.publish_req_chunked(flow_item, connections)
175174
else:

tableauserverclient/server/endpoint/workbooks_endpoint.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from .endpoint import QuerysetEndpoint, api, parameter_added_in
22
from .exceptions import InternalServerError, MissingRequiredFieldError
33
from .permissions_endpoint import _PermissionsEndpoint
4-
from .fileuploads_endpoint import Fileuploads
54
from .resource_tagger import _ResourceTagger
65
from .. import RequestFactory, WorkbookItem, ConnectionItem, ViewItem, PaginationItem
76
from ...models.job_item import JobItem
@@ -344,7 +343,7 @@ def publish(
344343
# Determine if chunking is required (64MB is the limit for single upload method)
345344
if file_size >= FILESIZE_LIMIT:
346345
logger.info("Publishing {0} to server with chunking method (workbook over 64MB)".format(workbook_item.name))
347-
upload_session_id = Fileuploads.upload_chunks(self.parent_srv, file)
346+
upload_session_id = self.parent_srv.fileuploads.upload(file)
348347
url = "{0}&uploadSessionId={1}".format(url, upload_session_id)
349348
conn_creds = connection_credentials
350349
xml_request, content_type = RequestFactory.Workbook.publish_req_chunked(

tableauserverclient/server/server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
DataAccelerationReport,
2525
Favorites,
2626
DataAlerts,
27+
Fileuploads,
2728
)
2829
from .endpoint.exceptions import (
2930
EndpointUnavailableError,
@@ -82,6 +83,7 @@ def __init__(self, server_address, use_server_version=False):
8283
self.webhooks = Webhooks(self)
8384
self.data_acceleration_report = DataAccelerationReport(self)
8485
self.data_alerts = DataAlerts(self)
86+
self.fileuploads = Fileuploads(self)
8587
self._namespace = Namespace()
8688

8789
if use_server_version:

test/test_fileuploads.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from ._utils import asset
66
from tableauserverclient.server import Server
7-
from tableauserverclient.server.endpoint.fileuploads_endpoint import Fileuploads
87

98
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets')
109
FILEUPLOAD_INITIALIZE = os.path.join(TEST_ASSET_DIR, 'fileupload_initialize.xml')
@@ -22,23 +21,18 @@ def setUp(self):
2221
self.baseurl = '{}/sites/{}/fileUploads'.format(self.server.baseurl, self.server.site_id)
2322

2423
def test_read_chunks_file_path(self):
25-
fileuploads = Fileuploads(self.server)
26-
2724
file_path = asset('SampleWB.twbx')
28-
chunks = fileuploads.read_chunks(file_path)
25+
chunks = self.server.fileuploads._read_chunks(file_path)
2926
for chunk in chunks:
3027
self.assertIsNotNone(chunk)
3128

3229
def test_read_chunks_file_object(self):
33-
fileuploads = Fileuploads(self.server)
34-
3530
with open(asset('SampleWB.twbx'), 'rb') as f:
36-
chunks = fileuploads.read_chunks(f)
31+
chunks = self.server.fileuploads._read_chunks(f)
3732
for chunk in chunks:
3833
self.assertIsNotNone(chunk)
3934

4035
def test_upload_chunks_file_path(self):
41-
fileuploads = Fileuploads(self.server)
4236
file_path = asset('SampleWB.twbx')
4337
upload_id = '7720:170fe6b1c1c7422dadff20f944d58a52-1:0'
4438

@@ -49,12 +43,11 @@ def test_upload_chunks_file_path(self):
4943
with requests_mock.mock() as m:
5044
m.post(self.baseurl, text=initialize_response_xml)
5145
m.put(self.baseurl + '/' + upload_id, text=append_response_xml)
52-
actual = fileuploads.upload_chunks(self.server, file_path)
46+
actual = self.server.fileuploads.upload(file_path)
5347

5448
self.assertEqual(upload_id, actual)
5549

5650
def test_upload_chunks_file_object(self):
57-
fileuploads = Fileuploads(self.server)
5851
upload_id = '7720:170fe6b1c1c7422dadff20f944d58a52-1:0'
5952

6053
with open(asset('SampleWB.twbx'), 'rb') as file_content:
@@ -65,6 +58,6 @@ def test_upload_chunks_file_object(self):
6558
with requests_mock.mock() as m:
6659
m.post(self.baseurl, text=initialize_response_xml)
6760
m.put(self.baseurl + '/' + upload_id, text=append_response_xml)
68-
actual = fileuploads.upload_chunks(self.server, file_content)
61+
actual = self.server.fileuploads.upload(file_content)
6962

7063
self.assertEqual(upload_id, actual)

0 commit comments

Comments
 (0)
0