Skip to content

Commit b77dad0

Browse files
authored
Merge pull request #319 from tableau/development
feature: custom views infra: automated release builds
2 parents 2744ca1 + 21da7e1 commit b77dad0

24 files changed

Lines changed: 559 additions & 165 deletions

.github/workflows/generate-metadata.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- name: Set up Python
1717
uses: actions/setup-python@v5
1818
with:
19-
python-version: '3.7'
19+
python-version: '3.9'
2020

2121
- name: Install App and Extras
2222
run: |

.github/workflows/package.yml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ name: Release-Executable
99
# https://anshumanfauzdar.medium.com/using-github-actions-to-bundle-python-application-into-a-single-package-and-automatic-release-834bd42e0670
1010

1111
on:
12-
release:
13-
types: [published]
12+
push:
13+
tags:
14+
- '*'
1415
workflow_dispatch:
1516

1617
jobs:
@@ -58,7 +59,7 @@ jobs:
5859

5960
- uses: actions/setup-python@v5
6061
with:
61-
python-version: 3.8
62+
python-version: 3.9
6263

6364
- name: Install dependencies and build
6465
run: |
@@ -94,10 +95,20 @@ jobs:
9495
with:
9596
name: ${{ matrix.OUT_FILE_NAME }}
9697
path: ./dist/${{ matrix.TARGET }}/${{ matrix.OUT_FILE_NAME }}
97-
98+
9899
- name: Upload artifact for Mac
99100
if: matrix.TARGET == 'macos'
100101
uses: actions/upload-artifact@v4
101102
with:
102103
name: ${{ matrix.BUNDLE_NAME }}
103104
path: ./dist/${{ matrix.TARGET }}/${{ matrix.BUNDLE_NAME }}.tar
105+
106+
- name: Upload binaries to release
107+
uses: svenstaro/upload-release-action@v2
108+
with:
109+
repo_token: ${{ secrets.GITHUB_TOKEN }}
110+
file: ./dist/${{ matrix.TARGET }}/${{ matrix.OUT_FILE_NAME }}/
111+
tag: ${{ github.ref_name }}
112+
overwrite: true
113+
promote: true
114+

.github/workflows/publish-pypi.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
fetch-depth: 0
2121
- uses: actions/setup-python@v5
2222
with:
23-
python-version: 3.8
23+
python-version: 3.9
2424
- name: Build dist files
2525
run: |
2626
python --version

.github/workflows/run-e2-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
fail-fast: true
1919
matrix:
2020
os: [ubuntu-latest, macos-latest, windows-latest]
21-
python-version: ['3.9', '3.10', '3']
21+
python-version: ['3.9', '3.10', '3.11', '3.12', '3']
2222

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

.github/workflows/run-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
fail-fast: true
1616
matrix:
1717
os: [ubuntu-latest, macos-latest, windows-latest]
18-
python-version: ['3.8', '3.9', '3.10', '3']
18+
python-version: ['3.9', '3.10', '3.11', '3.12', '3']
1919

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

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ dependencies = [
5252
"types-mock",
5353
"types-requests",
5454
"types-setuptools",
55-
"tableauserverclient==0.31",
55+
"tableauserverclient==0.34",
5656
"urllib3",
5757
]
5858
[project.optional-dependencies]

tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,14 @@ def save_to_file(logger, output, filename):
152152
with open(filename, "wb") as f:
153153
f.write(output)
154154
logger.info(_("export.success").format("", filename))
155+
156+
@staticmethod
157+
def get_custom_view_by_id(logger, server, custom_view_id) -> TSC.CustomViewItem:
158+
logger.debug(_("export.status").format(custom_view_id))
159+
try:
160+
matching_custom_view = server.custom_views.get_by_id(custom_view_id)
161+
except Exception as e:
162+
Errors.exit_with_error(logger, exception=e)
163+
if matching_custom_view is None:
164+
Errors.exit_with_error(logger, message=_("errors.xmlapi.not_found"))
165+
return matching_custom_view
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import os
2+
3+
from uuid import UUID
4+
5+
from tabcmd.commands.constants import Errors
6+
from tabcmd.commands.datasources_and_workbooks.datasources_and_workbooks_command import DatasourcesAndWorkbooks
7+
from tabcmd.commands.server import Server
8+
from tabcmd.execution.localize import _
9+
10+
11+
class DatasourcesWorkbooksAndViewsUrlParser(Server):
12+
"""
13+
Base Class for parsing & fetching Datasources, Workbooks, Views & Custom Views information from get/export URLs
14+
"""
15+
16+
def __init__(self, args):
17+
super().__init__(args)
18+
19+
@staticmethod
20+
def get_view_url_from_names(wb_name, view_name):
21+
return "{}/sheets/{}".format(wb_name, view_name)
22+
23+
@staticmethod
24+
def parse_export_url_to_workbook_view_and_custom_view(logger, url):
25+
# input should be workbook_name/view_name or /workbook_name/view_name
26+
# or workbook_name/view_name/custom_view_id/custom_view_name
27+
name_parts = DatasourcesWorkbooksAndViewsUrlParser.validate_and_extract_url_parts(logger, url)
28+
if len(name_parts) == 2:
29+
workbook = name_parts[0]
30+
view = DatasourcesWorkbooksAndViewsUrlParser.get_view_url_from_names(workbook, name_parts[1])
31+
return view, workbook, None, None
32+
elif len(name_parts) == 4:
33+
workbook = name_parts[0]
34+
view = DatasourcesWorkbooksAndViewsUrlParser.get_view_url_from_names(workbook, name_parts[1])
35+
custom_view_id = name_parts[2]
36+
DatasourcesWorkbooksAndViewsUrlParser.verify_valid_custom_view_id(logger, custom_view_id)
37+
custom_view_name = name_parts[3]
38+
return view, workbook, custom_view_id, custom_view_name
39+
else:
40+
return None, None, None, None
41+
42+
@staticmethod
43+
def validate_and_extract_url_parts(logger, url):
44+
logger.info(_("export.status").format(url))
45+
if " " in url:
46+
Errors.exit_with_error(logger, _("export.errors.white_space_workbook_view"))
47+
if "?" in url:
48+
url = url.split("?")[0]
49+
url = url.lstrip("/") # strip opening / if present
50+
return url.split("/")
51+
52+
@staticmethod
53+
def get_export_item_and_server_content_type_from_export_url(view_content_url, logger, server, custom_view_id):
54+
return DatasourcesWorkbooksAndViewsUrlParser.get_content_and_server_content_type_from_url(
55+
logger, server, view_content_url, custom_view_id
56+
)
57+
58+
################### GetURL Methods ##############################
59+
60+
@staticmethod
61+
def explain_expected_get_url(logger, url: str, command: str):
62+
view_example = "/views/<workbookname>/<viewname>[.ext]"
63+
custom_view_example = "/views/<workbookname>/<viewname>/<customviewid>/<customviewname>[.ext]"
64+
wb_example = "/workbooks/<workbookname>[.ext]"
65+
ds_example = "/datasources/<datasourcename[.ext]"
66+
message = _("export.errors.requires_workbook_view_param").format(
67+
command
68+
) + "Given: {0}. Accepted values: {1}, {2}, {3}, {4}".format(
69+
url, view_example, custom_view_example, wb_example, ds_example
70+
)
71+
Errors.exit_with_error(logger, message)
72+
73+
@staticmethod
74+
def get_file_type_from_filename(logger, url, file_name):
75+
logger.debug("Choosing between {}, {}".format(file_name, url))
76+
file_name = file_name or url
77+
logger.debug(_("get.options.file") + ": {}".format(file_name)) # Name to save the file as
78+
type_of_file = DatasourcesWorkbooksAndViewsUrlParser.get_file_extension(file_name)
79+
80+
if not type_of_file and file_name is not None:
81+
# check the url
82+
backup = DatasourcesWorkbooksAndViewsUrlParser.get_file_extension(url)
83+
if backup is not None:
84+
type_of_file = backup
85+
else:
86+
Errors.exit_with_error(logger, _("get.extension.not_found").format(file_name))
87+
88+
logger.debug("filetype: {}".format(type_of_file))
89+
if type_of_file in ["pdf", "csv", "png", "twb", "twbx", "tdsx", "tds"]:
90+
return type_of_file
91+
92+
Errors.exit_with_error(logger, _("get.extension.not_found").format(file_name))
93+
94+
@staticmethod
95+
def get_file_extension(path):
96+
path_segments = os.path.split(path)
97+
filename = path_segments[-1]
98+
filename_segments = filename.split(".")
99+
extension = filename_segments[-1]
100+
extension = DatasourcesWorkbooksAndViewsUrlParser.strip_query_params(extension)
101+
return extension
102+
103+
@staticmethod
104+
def strip_query_params(filename):
105+
if "?" in filename:
106+
return filename.split("?")[0]
107+
else:
108+
return filename
109+
110+
@staticmethod
111+
def get_name_without_possible_extension(filename):
112+
return filename.split(".")[0]
113+
114+
@staticmethod
115+
def get_resource_name(url: str, logger): # workbooks/wb-name" -> "wb-name", datasource/ds-name -> ds-name
116+
url = url.lstrip("/") # strip opening / if present
117+
name_parts = url.split("/")
118+
if len(name_parts) != 2:
119+
DatasourcesWorkbooksAndViewsUrlParser.explain_expected_get_url(logger, url, "GetUrl")
120+
resource_name_with_params = name_parts[::-1][0] # last part
121+
resource_name_with_ext = DatasourcesWorkbooksAndViewsUrlParser.strip_query_params(resource_name_with_params)
122+
resource_name = DatasourcesWorkbooksAndViewsUrlParser.get_name_without_possible_extension(
123+
resource_name_with_ext
124+
)
125+
return resource_name
126+
127+
@staticmethod
128+
def get_view_url_from_get_url(logger, url): # "views/wb-name/view-name" -> wb-name/sheets/view-name
129+
name_parts = url.split("/") # ['views', 'wb-name', 'view-name']
130+
if len(name_parts) != 3:
131+
DatasourcesWorkbooksAndViewsUrlParser.explain_expected_get_url(logger, url, "GetUrl")
132+
workbook_name = name_parts[1]
133+
view_name = name_parts[::-1][0]
134+
view_name = DatasourcesWorkbooksAndViewsUrlParser.strip_query_params(view_name)
135+
view_name = DatasourcesWorkbooksAndViewsUrlParser.get_name_without_possible_extension(view_name)
136+
return DatasourcesWorkbooksAndViewsUrlParser.get_view_url_from_names(workbook_name, view_name)
137+
138+
@staticmethod
139+
def get_custom_view_parts_from_get_url(logger, url):
140+
name_parts = url.split("/") # ['views', 'wb-name', 'view-name', 'custom-view-id', 'custom-view-name']
141+
if len(name_parts) != 5:
142+
DatasourcesWorkbooksAndViewsUrlParser.explain_expected_get_url(logger, url, "GetUrl")
143+
workbook_name = name_parts[1]
144+
view_name = name_parts[2]
145+
custom_view_id = name_parts[3]
146+
DatasourcesWorkbooksAndViewsUrlParser.verify_valid_custom_view_id(logger, custom_view_id)
147+
custom_view_name = name_parts[::-1][0]
148+
custom_view_name = DatasourcesWorkbooksAndViewsUrlParser.strip_query_params(custom_view_name)
149+
custom_view_name = DatasourcesWorkbooksAndViewsUrlParser.get_name_without_possible_extension(custom_view_name)
150+
return (
151+
DatasourcesWorkbooksAndViewsUrlParser.get_view_url_from_names(workbook_name, view_name),
152+
custom_view_id,
153+
custom_view_name,
154+
)
155+
156+
@staticmethod
157+
def parse_get_view_url_to_view_and_custom_view_parts(logger, url):
158+
# input should be views/workbook_name/view_name
159+
# or views/workbook_name/view_name/custom_view_id/custom_view_name
160+
name_parts = DatasourcesWorkbooksAndViewsUrlParser.validate_and_extract_url_parts(logger, url)
161+
if len(name_parts) == 3:
162+
return DatasourcesWorkbooksAndViewsUrlParser.get_view_url_from_get_url(logger, url), None, None
163+
elif len(name_parts) == 5:
164+
return DatasourcesWorkbooksAndViewsUrlParser.get_custom_view_parts_from_get_url(logger, url)
165+
else:
166+
DatasourcesWorkbooksAndViewsUrlParser.explain_expected_get_url(logger, url, "GetUrl")
167+
168+
@staticmethod
169+
def get_url_item_and_item_type_from_view_url(logger, url, server):
170+
(
171+
view_url,
172+
custom_view_id,
173+
custom_view_name,
174+
) = DatasourcesWorkbooksAndViewsUrlParser.parse_get_view_url_to_view_and_custom_view_parts(logger, url)
175+
176+
return DatasourcesWorkbooksAndViewsUrlParser.get_content_and_server_content_type_from_url(
177+
logger, server, view_url, custom_view_id
178+
)
179+
180+
@staticmethod
181+
def get_content_and_server_content_type_from_url(logger, server, view_content_url, custom_view_id):
182+
item = DatasourcesAndWorkbooks.get_view_by_content_url(logger, server, view_content_url)
183+
server_content_type = server.views
184+
185+
if custom_view_id:
186+
custom_view_item = DatasourcesAndWorkbooks.get_custom_view_by_id(logger, server, custom_view_id)
187+
if custom_view_item.view.id != item.id:
188+
Errors.exit_with_error(logger, "Invalid custom view URL provided")
189+
server_content_type = server.custom_views
190+
item = custom_view_item
191+
return item, server_content_type
192+
193+
@staticmethod
194+
def verify_valid_custom_view_id(logger, custom_view_id):
195+
try:
196+
UUID(custom_view_id)
197+
except ValueError:
198+
Errors.exit_with_error(logger, _("export.errors.requires_valid_custom_view_uuid"))

0 commit comments

Comments
 (0)