Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for custom view urls to export & get tabcmd commands #313

Merged
merged 7 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/generate-metadata.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.7'
python-version: '3.9'

- name: Install App and Extras
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:

- uses: actions/setup-python@v5
with:
python-version: 3.8
python-version: 3.9

- name: Install dependencies and build
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: 3.8
python-version: 3.9
- name: Build dist files
run: |
python --version
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ['3.8', '3.9', '3.10', '3']
python-version: ['3.9', '3.10', '3']

runs-on: ${{ matrix.os }}

Expand Down
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
90 changes: 64 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,12 @@ 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 +99,20 @@ 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 +128,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 +164,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"))
Loading