10000 File objects for publish workbook (#704) · tableau/server-client-python@1f4e27f · GitHub
[go: up one dir, main page]

Skip to content

Commit 1f4e27f

Browse files
authored
File objects for publish workbook (#704)
Contributed by @nnevalainen, take a file path or a file object for workbook publish
1 parent 6d52669 commit 1f4e27f

File tree

6 files changed

+3731
-33
lines changed

6 files changed

+3731
-33
lines changed

tableauserverclient/filesys_helpers.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,23 @@ def make_download_path(filepath, filename):
2020
download_path = filepath + os.path.splitext(filename)[1]
2121

2222
return download_path
23+
24+
25+
def get_file_object_size(file):
26+
# Returns the size of a file object
27+
file.seek(0, os.SEEK_END)
28+
file_size = file.tell()
29+
file.seek(0)
30+
return file_size
31+
32+
33+
def file_is_compressed(file):
34+
# Determine if file is a zip file or not
35+
# This reference lists magic file signatures: https://www.garykessler.net/library/file_sigs.html
36+
37+
zip_file_signature = b'PK\x03\x04'
38+
39+
is_zip_file = file.read(len(zip_file_signature)) == zip_file_signature
40+
file.seek(0)
41+
42+
return is_zip_file

tableauserverclient/server/endpoint/fileuploads_endpoint.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,27 @@ def append(self, xml_request, content_type):
3939
logger.info('Uploading a chunk to session (ID: {0})'.format(self.upload_id))
4040
return FileuploadItem.from_response(server_response.content, self.parent_srv.namespace)
4141

42-
def read_chunks(self, file_path):
43-
with open(file_path, 'rb') as f:
44-
while True:
45-
chunked_content = f.read(CHUNK_SIZE)
46-
if not chunked_content:
47-
break
48-
yield chunked_content
42+
def read_chunks(self, file):
43+
44+
while True:
45+
chunked_content = file.read(CHUNK_SIZE)
46+
if not chunked_content:
47+
break
48+
yield chunked_content
4949

5050
@classmethod
51-
def upload_chunks(cls, parent_srv, file_path):
51+
def upload_chunks(cls, parent_srv, file):
5252
file_uploader = cls(parent_srv)
5353
upload_id = file_uploader.initiate()
54-
chunks = file_uploader.read_chunks(file_path)
54+
55+
try:
56+
with open(file, 'rb') as f:
57+
chunks = file_uploader.read_chunks(f)
58+
except TypeError:
59+
chunks = file_uploader.read_chunks(file)
5560
for chunk in chunks:
5661
xml_request, content_type = RequestFactory.Fileupload.chunk_req(chunk)
5762
fileupload_item = file_uploader.append(xml_request, content_type)
58-
logger.info("\tPublished {0}MB of {1}".format(fileupload_item.file_size,
59-
os.path.basename(file_path)))
63+
logger.info("\tPublished {0}MB".format(fileupload_item.file_size))
6064
logger.info("\tCommitting file upload...")
6165
return upload_id

tableauserverclient/server/endpoint/workbooks_endpoint.py

Lines changed: 45 additions & 22 deletions
326
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from .resource_tagger import _ResourceTagger
66
from .. import RequestFactory, WorkbookItem, ConnectionItem, ViewItem, PaginationItem
77
from ...models.job_item import JobItem
8-
from ...filesys_helpers import to_filename, make_download_path
8+
from ...filesys_helpers import to_filename, make_download_path, file_is_compressed, get_file_object_size
99

1010
import os
1111
import logging
@@ -254,7 +254,7 @@ def delete_permission(self, item, capability_item):
254254
@parameter_added_in(as_job='3.0')
255255
@parameter_added_in(connections='2.8')
256256
def publish(
257-
self, workbook_item, file_path, mode,
257+
self, workbook_item, file, mode,
258258
connection_credentials=None, connections=None, as_job=False,
259259
hidden_views=None
260260
):
@@ -264,23 +264,40 @@ def publish(
264264
warnings.warn("connection_credentials is being deprecated. Use connections instead",
265265
DeprecationWarning)
266266

267-
if not os.path.isfile(file_path):
268-
error = "File path does not lead to an existing file."
269-
raise IOError(error)
267+
try:
268+
# Expect file to be a filepath
269+
if not os.path.isfile(file):
270+
error = "File path does not lead to an existing file."
271+
raise IOError(error)
272+
273+
filename = os.path.basename(file)
274+
file_extension = os.path.splitext(filename)[1][1:]
275+
file_size = os.path.getsize(file)
276+
277+
# If name is not defined, grab the name from the file to publish
278+
if not workbook_item.name:
279+
workbook_item.name = os.path.splitext(filename)[0]
280+
if file_extension not in ALLOWED_FILE_EXTENSIONS:
281+
error = "Only {} files can be published as workbooks.".format(', '.join(ALLOWED_FILE_EXTENSIONS))
282+
raise ValueError(error)
283+
284+
except TypeError:
285+
# Expect file to be a file object
286+
file_size = get_file_object_size(file)
287+
file_extension = 'twbx' if file_is_compressed(file) else 'twb'
288+
289+
if not workbook_item.name:
290+
error = "Workbook item must have a name when passing a file object"
291+
raise ValueError(error)
292+
293+
# Generate filename for file object.
294+
# This is needed when publishing the workbook in a single request
295+
filename = "{}.{}".format(workbook_item.name, file_extension)
296+
270297
if not hasattr(self.parent_srv.PublishMode, mode):
271298
error = 'Invalid mode defined.'
272299
raise ValueError(error)
273300

274-
filename = os.path.basename(file_path)
275-
file_extension = os.path.splitext(filename)[1][1:]
276-
277-
# If name is not defined, grab the name from the file to publish
278-
if not workbook_item.name:
279-
workbook_item.name = os.path.splitext(filename)[0]
280-
if file_extension not in ALLOWED_FILE_EXTENSIONS:
281-
error = "Only {} files can be published as workbooks.".format(', '.join(ALLOWED_FILE_EXTENSIONS))
282-
raise ValueError(error)
283-
284301
# Construct the url with the defined mode
285302
url = "{0}?workbookType={1}".format(self.baseurl, file_extension)
286303
if mode == self.parent_srv.PublishMode.Overwrite:
@@ -293,9 +310,9 @@ def publish(
293310
url += '&{0}=true'.format('asJob')
294311

295312
# Determine if chunking is required (64MB is the limit for single upload method)
296-
if os.path.getsize(file_path) >= FILESIZE_LIMIT:
297-
logger.info('Publishing {0} to server with chunking method (workbook over 64MB)'.format(filename))
298-
upload_session_id = Fileuploads.upload_chunks(self.parent_srv, file_path)
313+
if file_size >= FILESIZE_LIMIT:
314+
logger.info('Publishing {0} to server with chunking method (workbook over 64MB)'.format(workbook_item.name))
315+
upload_session_id = Fileuploads.upload_chunks(self.parent_srv, file)
299316
url = "{0}&uploadSessionId={1}".format(url, upload_session_id)
300317
conn_creds = connection_credentials
301318
xml_request, content_type = RequestFactory.Workbook.publish_req_chunked(workbook_item,
@@ -304,8 +321,14 @@ def publish(
304321
hidden_views=hidden_views)
305322
else:
306323
logger.info('Publishing {0} to server'.format(filename))
307-
with open(file_path, 'rb') as f:
308-
file_contents = f.read()
324+
325+
try:
+
with open(file, 'rb') as f:
327+
file_contents = f.read()
328+
329+
except TypeError:
330+
file_contents = file.read()
331+
309332
conn_creds = connection_credentials
310333
xml_request, content_type = RequestFactory.Workbook.publish_req(workbook_item,
311334
filename,
@@ -325,9 +348,9 @@ def publish(
325348

326349
if as_job:
327350
new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
328-
logger.info('Published {0} (JOB_ID: {1}'.format(filename, new_job.id))
351+
logger.info('Published {0} (JOB_ID: {1}'.format(workbook_item.name, new_job.id))
329352
return new_job
330353
else:
331354
new_workbook = WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)[0]
332-
logger.info('Published {0} (ID: {1})'.format(filename, new_workbook.id))
355+
logger.info('Published {0} (ID: {1})'.format(workbook_item.name, new_workbook.id))
333356
return new_workbook
Binary file not shown.

0 commit comments

Comments
 (0)
0