Skip to content

Commit

Permalink
Adding support for export for custom views to pdf,png & csv formats
Browse files Browse the repository at this point in the history
  • Loading branch information
renoyjohnm committed Nov 13, 2024
1 parent e1f80d3 commit e3aab5e
Show file tree
Hide file tree
Showing 7 changed files with 325 additions and 59 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ dependencies = [
"types-mock",
"types-requests",
"types-setuptools",
"tableauserverclient==0.31",
"tableauserverclient==0.34",
"urllib3",
]
[project.optional-dependencies]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import urllib

import tableauserverclient as TSC
from tableauserverclient import CustomViewItem

from tabcmd.commands.constants import Errors
from tabcmd.commands.server import Server
Expand Down Expand Up @@ -152,3 +153,14 @@ def save_to_file(logger, output, filename):
with open(filename, "wb") as f:
f.write(output)
logger.info(_("export.success").format("", filename))

@staticmethod
def get_custom_view_by_id(logger, server, custom_view_id) -> TSC.CustomViewItem:
logger.debug(_("export.status").format(custom_view_id))
try:
matching_custom_view = server.custom_views.get_by_id(custom_view_id)
except Exception as e:
Errors.exit_with_error(logger, exception=e)
if matching_custom_view is None:
Errors.exit_with_error(logger, message=_("errors.xmlapi.not_found"))
return matching_custom_view
85 changes: 59 additions & 26 deletions tabcmd/commands/datasources_and_workbooks/export_command.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import tableauserverclient as TSC

from uuid import UUID

from tabcmd.commands.auth.session import Session
from tabcmd.commands.constants import Errors
from tabcmd.execution.localize import _
Expand Down Expand Up @@ -76,7 +78,8 @@ def run_command(args):
logger.debug(_("tabcmd.launching"))
session = Session()
server = session.create_session(args, logger)
view_content_url, wb_content_url = ExportCommand.parse_export_url_to_workbook_and_view(logger, args.url)
view_content_url, wb_content_url, custom_view_id, custom_view_name = (
ExportCommand.parse_export_url_to_workbook_view_and_custom_view(logger, args.url))
logger.debug(["view_url:", view_content_url, "workbook:", wb_content_url])
if not view_content_url and not wb_content_url:
view_example = "/workbook_name/view_name"
Expand All @@ -92,19 +95,19 @@ def run_command(args):

default_filename = "{}.pdf".format(workbook_item.name)

elif args.pdf or args.png or args.csv: # it's a view
view_item = ExportCommand.get_view_by_content_url(logger, server, view_content_url)
elif args.pdf or args.png or args.csv: # it's a view or custom_view
export_item, server_content_type = ExportCommand.get_export_item_and_server_content_type(
view_content_url, logger, server, custom_view_id)

if args.pdf:
output = ExportCommand.download_view_pdf(server, view_item, args, logger)
default_filename = "{}.pdf".format(view_item.name)
output = ExportCommand.download_view_pdf(server_content_type, export_item, args, logger)
default_filename = "{}.pdf".format(export_item.name)
elif args.csv:
output = ExportCommand.download_csv(server, view_item, args, logger)
default_filename = "{}.csv".format(view_item.name)
output = ExportCommand.download_csv(server_content_type, export_item, args, logger)
default_filename = "{}.csv".format(export_item.name)
elif args.png:
output = ExportCommand.download_png(server, view_item, args, logger)

default_filename = "{}.png".format(view_item.name)
output = ExportCommand.download_png(server_content_type, export_item, args, logger)
default_filename = "{}.png".format(export_item.name)

except TSC.ServerResponseError as e:
Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format(""), e)
Expand All @@ -120,6 +123,20 @@ def run_command(args):
except Exception as e:
Errors.exit_with_error(logger, "Error saving to file", e)

@staticmethod
def get_export_item_and_server_content_type(view_content_url, logger, server, custom_view_id):
export_item = ExportCommand.get_view_by_content_url(logger, server, view_content_url)
server_content_type = server.views

if custom_view_id:
custom_view_item = ExportCommand.get_custom_view_by_id(logger, server, custom_view_id)
if custom_view_item.view.id != export_item.id:
Errors.exit_with_error(logger, "Invalid custom view URL provided")
server_content_type = server.custom_views
export_item = custom_view_item

return export_item, server_content_type

@staticmethod
def apply_filters_from_args(request_options: TSC.PDFRequestOptions, args, logger=None) -> None:
if args.filter:
Expand All @@ -142,51 +159,67 @@ def download_wb_pdf(server, workbook_item, args, logger):
return workbook_item.pdf

@staticmethod
def download_view_pdf(server, view_item, args, logger):
def download_view_pdf(server_content_type, export_item, args, logger):
logger.debug(args.url)
pdf_options = TSC.PDFRequestOptions(maxage=1)
ExportCommand.apply_values_from_url_params(logger, pdf_options, args.url)
ExportCommand.apply_filters_from_args(pdf_options, args, logger)
ExportCommand.apply_pdf_options(logger, pdf_options, args)
logger.debug(pdf_options.get_query_params())
server.views.populate_pdf(view_item, pdf_options)
return view_item.pdf
server_content_type.populate_pdf(export_item, pdf_options)
return export_item.pdf

@staticmethod
def download_csv(server, view_item, args, logger):
def download_csv(server_content_type, export_item, args, logger):
logger.debug(args.url)
csv_options = TSC.CSVRequestOptions(maxage=1)
ExportCommand.apply_values_from_url_params(logger, csv_options, args.url)
ExportCommand.apply_filters_from_args(csv_options, args, logger)
logger.debug(csv_options.get_query_params())
server.views.populate_csv(view_item, csv_options)
return view_item.csv
server_content_type.populate_csv(export_item, csv_options)
return export_item.csv

@staticmethod
def download_png(server, view_item, args, logger):
def download_png(server_content_type, export_item, args, logger):
logger.debug(args.url)
image_options = TSC.ImageRequestOptions(maxage=1)
ExportCommand.apply_values_from_url_params(logger, image_options, args.url)
ExportCommand.apply_filters_from_args(image_options, args, logger)
DatasourcesAndWorkbooks.apply_png_options(logger, image_options, args)
logger.debug(image_options.get_query_params())
server.views.populate_image(view_item, image_options)
return view_item.image
server_content_type.populate_image(export_item, image_options)
return export_item.image

@staticmethod
def parse_export_url_to_workbook_and_view(logger, url):
def parse_export_url_to_workbook_view_and_custom_view(logger, url):
logger.info(_("export.status").format(url))
if " " in url:
Errors.exit_with_error(logger, _("export.errors.white_space_workbook_view"))
if "?" in url:
url = url.split("?")[0]
# input should be workbook_name/view_name or /workbook_name/view_name
# or workbook_name/view_name/custom_view_id/custom_view_name
url = url.lstrip("/") # strip opening / if present
if not url.find("/"):
return None, None
return None, None, None, None
name_parts = url.split("/")
if len(name_parts) != 2:
return None, None
workbook = name_parts[0]
view = "{}/sheets/{}".format(workbook, name_parts[1])
return view, workbook
if len(name_parts) == 2:
workbook = name_parts[0]
view = "{}/sheets/{}".format(workbook, name_parts[1])
return view, workbook, None, None
elif len(name_parts) == 4:
workbook = name_parts[0]
view = "{}/sheets/{}".format(workbook, name_parts[1])
custom_view_id = name_parts[2]
ExportCommand.verify_valid_custom_view_id(logger, custom_view_id)
custom_view_name = name_parts[3]
return view, workbook, custom_view_id, custom_view_name
else:
return None, None, None, None

@staticmethod
def verify_valid_custom_view_id(logger, custom_view_id):
try:
UUID(custom_view_id)
except ValueError:
Errors.exit_with_error(logger, _("export.errors.requires_valid_custom_view_uuid"))
104 changes: 78 additions & 26 deletions tabcmd/commands/datasources_and_workbooks/get_url_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
import os

import tableauserverclient as TSC
from tableauserverclient import ServerResponseError

from tabcmd.commands.auth.session import Session
from tabcmd.commands.constants import Errors
from tabcmd.execution.global_options import *
from tabcmd.execution.localize import _
from tabcmd.execution.logger_config import log
from .datasources_and_workbooks_command import DatasourcesAndWorkbooks
from .export_command import ExportCommand


class GetUrl(DatasourcesAndWorkbooks):
Expand Down Expand Up @@ -68,11 +68,13 @@ def evaluate_content_type(logger, url):
@staticmethod
def explain_expected_url(logger, url: str, command: str):
view_example = "/views/<workbookname>/<viewname>[.ext]"
custom_view_example = "/views/<workbookname>/<viewname>/<customviewid>/<customviewname>[.ext]"
wb_example = "/workbooks/<workbookname>[.ext]"
ds_example = "/datasources/<datasourcename[.ext]"
message = _("export.errors.requires_workbook_view_param").format(
command
) + "Given: {0}. Accepted values: {1}, {2}, {3}".format(url, view_example, wb_example, ds_example)
) + "Given: {0}. Accepted values: {1}, {2}, {3}, {4}".format(url, view_example, custom_view_example,
wb_example, ds_example)
Errors.exit_with_error(logger, message)

@staticmethod
Expand Down Expand Up @@ -138,6 +140,21 @@ def get_view_url(url, logger): # "views/wb-name/view-name" -> wb-name/sheets/vi
view_name = GetUrl.get_name_without_possible_extension(view_name)
return DatasourcesAndWorkbooks.get_view_url_from_names(workbook_name, view_name)

@staticmethod
def get_url_parts_from_custom_view_url(url, logger):
name_parts = url.split("/") # ['views', 'wb-name', 'view-name', 'custom-view-id', 'custom-view-name']
if len(name_parts) != 5:
GetUrl.explain_expected_url(logger, url, "GetUrl")
workbook_name = name_parts[1]
view_name = name_parts[2]
custom_view_id = name_parts[3]
ExportCommand.verify_valid_custom_view_id(logger, custom_view_id)
custom_view_name = name_parts[::-1][0]
custom_view_name = GetUrl.strip_query_params(custom_view_name)
custom_view_name = GetUrl.get_name_without_possible_extension(custom_view_name)
return (DatasourcesAndWorkbooks.get_view_url_from_names(workbook_name, view_name), custom_view_id,
custom_view_name)

@staticmethod
def filename_from_args(file_argument, item_name, filetype):
if file_argument is None:
Expand All @@ -157,55 +174,53 @@ def get_content_as_file(file_type, content_type, logger, args, server, url):
elif content_type == "datasource":
return GetUrl.generate_tds(logger, server, args, file_type)
elif content_type == "view":
view_url = GetUrl.get_view_url(url, logger)
get_url_item, server_content_type = GetUrl.get_url_item_and_item_type_from_view_url(logger, url, server)

if file_type == "pdf":
return GetUrl.generate_pdf(logger, server, args, view_url)
return GetUrl.generate_pdf(logger, server_content_type, args, get_url_item)
elif file_type == "png":
return GetUrl.generate_png(logger, server, args, view_url)
return GetUrl.generate_png(logger, server_content_type, args, get_url_item)
elif file_type == "csv":
return GetUrl.generate_csv(logger, server, args, view_url)
return GetUrl.generate_csv(logger, server_content_type, args, get_url_item)
# all the known options above will return early. If we get here we are confused.
Errors.exit_with_error(logger, message=_("get.extension.not_found"))

@staticmethod
def generate_pdf(logger, server, args, view_url):
def generate_pdf(logger, server_content_type, args, get_url_item):
logger.trace("Entered method " + inspect.stack()[0].function)
try:
view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view_url)
logger.debug(_("content_type.view") + ": {}".format(view_item.name))
logger.debug(_("content_type.view") + ": {}".format(get_url_item.name))
req_option_pdf = TSC.PDFRequestOptions(maxage=1)
DatasourcesAndWorkbooks.apply_values_from_url_params(logger, req_option_pdf, args.url)
server.views.populate_pdf(view_item, req_option_pdf)
filename = GetUrl.filename_from_args(args.filename, view_item.name, "pdf")
DatasourcesAndWorkbooks.save_to_file(logger, view_item.pdf, filename)
server_content_type.populate_pdf(get_url_item, req_option_pdf)
filename = GetUrl.filename_from_args(args.filename, get_url_item.name, "pdf")
DatasourcesAndWorkbooks.save_to_file(logger, get_url_item.pdf, filename)
except Exception as e:
Errors.exit_with_error(logger, exception=e)

@staticmethod
def generate_png(logger, server, args, view_url):
def generate_png(logger, server_content_type, args, get_url_item):
logger.trace("Entered method " + inspect.stack()[0].function)
try:
view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view_url)
logger.debug(_("content_type.view") + ": {}".format(view_item.name))
logger.debug(_("content_type.view") + ": {}".format(get_url_item.name))
req_option_csv = TSC.ImageRequestOptions(maxage=1)
DatasourcesAndWorkbooks.apply_values_from_url_params(logger, req_option_csv, args.url)
server.views.populate_image(view_item, req_option_csv)
filename = GetUrl.filename_from_args(args.filename, view_item.name, "png")
DatasourcesAndWorkbooks.save_to_file(logger, view_item.image, filename)
server_content_type.populate_image(get_url_item, req_option_csv)
filename = GetUrl.filename_from_args(args.filename, get_url_item.name, "png")
DatasourcesAndWorkbooks.save_to_file(logger, get_url_item.image, filename)
except Exception as e:
Errors.exit_with_error(logger, exception=e)

@staticmethod
def generate_csv(logger, server, args, view_url):
def generate_csv(logger, server_content_type, args, get_url_item):
logger.trace("Entered method " + inspect.stack()[0].function)
try:
view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view_url)
logger.debug(_("content_type.view") + ": {}".format(view_item.name))
logger.debug(_("content_type.view") + ": {}".format(get_url_item.name))
req_option_csv = TSC.CSVRequestOptions(maxage=1)
DatasourcesAndWorkbooks.apply_values_from_url_params(logger, req_option_csv, args.url)
server.views.populate_csv(view_item, req_option_csv)
file_name_with_path = GetUrl.filename_from_args(args.filename, view_item.name, "csv")
DatasourcesAndWorkbooks.save_to_data_file(logger, view_item.csv, file_name_with_path)
server_content_type.populate_csv(get_url_item, req_option_csv)
file_name_with_path = GetUrl.filename_from_args(args.filename, get_url_item.name, "csv")
DatasourcesAndWorkbooks.save_to_data_file(logger, get_url_item.csv, file_name_with_path)
except Exception as e:
Errors.exit_with_error(logger, exception=e)

Expand All @@ -221,7 +236,7 @@ def generate_twb(logger, server, args, file_extension, url):
file_name_with_path = GetUrl.get_name_without_possible_extension(file_name_with_path)
file_name_with_ext = "{}.{}".format(file_name_with_path, file_extension)
logger.debug("Saving as {}".format(file_name_with_ext))
server.workbooks.download(target_workbook.id, filepath=file_name_with_path, no_extract=False)
server.workbooks.download(target_workbook.id, filepath=file_name_with_path, include_extract=False)
logger.info(_("export.success").format(target_workbook.name, file_name_with_ext))
except Exception as e:
Errors.exit_with_error(logger, exception=e)
Expand All @@ -238,7 +253,44 @@ def generate_tds(logger, server, args, file_extension):
file_name_with_path = GetUrl.get_name_without_possible_extension(file_name_with_path)
file_name_with_ext = "{}.{}".format(file_name_with_path, file_extension)
logger.debug("Saving as {}".format(file_name_with_ext))
server.datasources.download(target_datasource.id, filepath=file_name_with_path, no_extract=False)
server.datasources.download(target_datasource.id, filepath=file_name_with_path, include_extract=False)
logger.info(_("export.success").format(target_datasource.name, file_name_with_ext))
except Exception as e:
Errors.exit_with_error(logger, exception=e)

@staticmethod
def parse_get_view_url_to_view_and_custom_view_parts(logger, url):
logger.info(_("export.status").format(url))
if " " in url:
Errors.exit_with_error(logger, _("export.errors.white_space_workbook_view"))
if "?" in url:
url = url.split("?")[0]
# input should be views/workbook_name/view_name
# or views/workbook_name/view_name/custom_view_id/custom_view_name
url = url.lstrip("/") # strip opening / if present
if not url.find("/"):
GetUrl.explain_expected_url(logger, url, "GetUrl")
name_parts = url.split("/")
if len(name_parts) == 3:
return GetUrl.get_view_url(url, logger), None, None
elif len(name_parts) == 5:
return GetUrl.get_url_parts_from_custom_view_url(url, logger)
else:
GetUrl.explain_expected_url(logger, url, "GetUrl")

@staticmethod
def get_url_item_and_item_type_from_view_url(logger, url, server):
view_url, custom_view_id, custom_view_name = GetUrl.parse_get_view_url_to_view_and_custom_view_parts(
logger, url)

get_url_item = GetUrl.get_view_by_content_url(logger, server, view_url)
get_url_item_type = server.views

if custom_view_id:
custom_view_item = GetUrl.get_custom_view_by_id(logger, server, custom_view_id)
if custom_view_item.view.id != get_url_item.id:
Errors.exit_with_error(logger, "invalid custom view id provided")
get_url_item = custom_view_item
get_url_item_type = server.custom_views

return get_url_item, get_url_item_type
Loading

0 comments on commit e3aab5e

Please sign in to comment.