From 1f9088f7637b46214fd98c2db43249d38c7d66c4 Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Fri, 1 Dec 2023 19:43:20 -0600 Subject: [PATCH 1/4] fix: correct type hint on download_revision revision_number --- tableauserverclient/server/endpoint/workbooks_endpoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py index 3c8efbe3b..dbcc1ec53 100644 --- a/tableauserverclient/server/endpoint/workbooks_endpoint.py +++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py @@ -455,7 +455,7 @@ def _get_workbook_revisions( def download_revision( self, workbook_id: str, - revision_number: str, + revision_number: Optional[str], filepath: Optional[PathOrFileW] = None, include_extract: bool = True, no_extract: Optional[bool] = None, From f42948a1bee9e7f122764ecc2c9cf1c9d6877ea1 Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Sat, 9 Dec 2023 22:01:37 -0600 Subject: [PATCH 2/4] fix: handle filename* in download response --- .gitignore | 2 ++ tableauserverclient/helpers/headers.py | 19 +++++++++++++++++++ .../server/endpoint/datasources_endpoint.py | 3 +++ .../server/endpoint/flows_endpoint.py | 3 +++ .../server/endpoint/workbooks_endpoint.py | 3 +++ test/test_datasource.py | 14 ++++++++++++++ test/test_flow.py | 15 +++++++++++++++ test/test_workbook.py | 14 ++++++++++++++ 8 files changed, 73 insertions(+) create mode 100644 tableauserverclient/helpers/headers.py diff --git a/.gitignore b/.gitignore index f0226c065..e9bd2b49f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ var/ *.egg-info/ .installed.cfg *.egg +pip-wheel-metadata/ # PyInstaller # Usually these files are written by a python script from a template @@ -89,6 +90,7 @@ env.py # virtualenv venv/ ENV/ +.venv/ # Spyder project settings .spyderproject diff --git a/tableauserverclient/helpers/headers.py b/tableauserverclient/helpers/headers.py new file mode 100644 index 000000000..18b4eacd6 --- /dev/null +++ b/tableauserverclient/helpers/headers.py @@ -0,0 +1,19 @@ +from copy import deepcopy +from typing import Any, Generic, Mapping, Optional, TypeVar, Union +from urllib.parse import unquote_plus + +T = TypeVar("T", ) + +def fix_filename(params: Mapping[str, T]) -> Mapping[str, T]: + if "filename*" not in params: + return params + + params = deepcopy(params) + filename = params["filename*"] + prefix = "UTF-8''" + if filename.startswith(prefix): + filename = filename[len(prefix):] + + params["filename"] = unquote_plus(filename) + del params["filename*"] + return params \ No newline at end of file diff --git a/tableauserverclient/server/endpoint/datasources_endpoint.py b/tableauserverclient/server/endpoint/datasources_endpoint.py index c60f8f919..66ad9f710 100644 --- a/tableauserverclient/server/endpoint/datasources_endpoint.py +++ b/tableauserverclient/server/endpoint/datasources_endpoint.py @@ -8,6 +8,8 @@ from pathlib import Path from typing import List, Mapping, Optional, Sequence, Tuple, TYPE_CHECKING, Union +from tableauserverclient.helpers.headers import fix_filename + if TYPE_CHECKING: from tableauserverclient.server import Server from tableauserverclient.models import PermissionsRule @@ -441,6 +443,7 @@ def download_revision( filepath.write(chunk) return_path = filepath else: + params = fix_filename(params) filename = to_filename(os.path.basename(params["filename"])) download_path = make_download_path(filepath, filename) with open(download_path, "wb") as f: diff --git a/tableauserverclient/server/endpoint/flows_endpoint.py b/tableauserverclient/server/endpoint/flows_endpoint.py index ba8a152d7..21c16b1cc 100644 --- a/tableauserverclient/server/endpoint/flows_endpoint.py +++ b/tableauserverclient/server/endpoint/flows_endpoint.py @@ -7,6 +7,8 @@ from pathlib import Path from typing import Iterable, List, Optional, TYPE_CHECKING, Tuple, Union +from tableauserverclient.helpers.headers import fix_filename + from .dqw_endpoint import _DataQualityWarningEndpoint from .endpoint import QuerysetEndpoint, api from .exceptions import InternalServerError, MissingRequiredFieldError @@ -124,6 +126,7 @@ def download(self, flow_id: str, filepath: Optional[PathOrFileW] = None) -> Path filepath.write(chunk) return_path = filepath else: + params = fix_filename(params) filename = to_filename(os.path.basename(params["filename"])) download_path = make_download_path(filepath, filename) with open(download_path, "wb") as f: diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py index dbcc1ec53..506fe02c2 100644 --- a/tableauserverclient/server/endpoint/workbooks_endpoint.py +++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py @@ -6,6 +6,8 @@ from contextlib import closing from pathlib import Path +from tableauserverclient.helpers.headers import fix_filename + from .endpoint import QuerysetEndpoint, api, parameter_added_in from .exceptions import InternalServerError, MissingRequiredFieldError from .permissions_endpoint import _PermissionsEndpoint @@ -487,6 +489,7 @@ def download_revision( filepath.write(chunk) return_path = filepath else: + params = fix_filename(params) filename = to_filename(os.path.basename(params["filename"])) download_path = make_download_path(filepath, filename) with open(download_path, "wb") as f: diff --git a/test/test_datasource.py b/test/test_datasource.py index e299e5291..c79bf45fd 100644 --- a/test/test_datasource.py +++ b/test/test_datasource.py @@ -696,3 +696,17 @@ def test_download_revision(self) -> None: ) file_path = self.server.datasources.download_revision("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", "3", td) self.assertTrue(os.path.exists(file_path)) + + def test_bad_download_response(self) -> None: + with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: + m.get( + self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", + headers={ + "Content-Disposition": '''name="tableau_datasource"; filename*=UTF-8''"Sample datasource.tds"''' + } + ) + file_path = self.server.datasources.download( + "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", + td + ) + self.assertTrue(os.path.exists(file_path)) \ No newline at end of file diff --git a/test/test_flow.py b/test/test_flow.py index d10641809..d7fa2dbc3 100644 --- a/test/test_flow.py +++ b/test/test_flow.py @@ -1,5 +1,6 @@ import os import requests_mock +import tempfile import unittest from io import BytesIO @@ -203,3 +204,17 @@ def test_refresh(self): self.assertEqual(refresh_job.flow_run.id, "e0c3067f-2333-4eee-8028-e0a56ca496f6") self.assertEqual(refresh_job.flow_run.flow_id, "92967d2d-c7e2-46d0-8847-4802df58f484") self.assertEqual(format_datetime(refresh_job.flow_run.started_at), "2018-05-22T13:00:29Z") + + def test_bad_download_response(self) -> None: + with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: + m.get( + self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", + headers={ + "Content-Disposition": '''name="tableau_flow"; filename*=UTF-8''"Sample flow.tfl"''' + } + ) + file_path = self.server.flows.download( + "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", + td + ) + self.assertTrue(os.path.exists(file_path)) diff --git a/test/test_workbook.py b/test/test_workbook.py index 5114ce1b8..9804b2c02 100644 --- a/test/test_workbook.py +++ b/test/test_workbook.py @@ -932,3 +932,17 @@ def test_download_revision(self) -> None: ) file_path = self.server.workbooks.download_revision("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", "3", td) self.assertTrue(os.path.exists(file_path)) + + def test_bad_download_response(self) -> None: + with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: + m.get( + self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", + headers={ + "Content-Disposition": '''name="tableau_workbook"; filename*=UTF-8''"Sample workbook.twb"''' + } + ) + file_path = self.server.workbooks.download( + "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", + td + ) + self.assertTrue(os.path.exists(file_path)) From 76559d4c0456034a610818fd3bace51067e7ba07 Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Sat, 9 Dec 2023 22:06:05 -0600 Subject: [PATCH 3/4] style: black formatting --- tableauserverclient/helpers/headers.py | 11 +++++++---- test/test_datasource.py | 9 +++------ test/test_flow.py | 9 ++------- test/test_workbook.py | 9 ++------- 4 files changed, 14 insertions(+), 24 deletions(-) diff --git a/tableauserverclient/helpers/headers.py b/tableauserverclient/helpers/headers.py index 18b4eacd6..57be21b23 100644 --- a/tableauserverclient/helpers/headers.py +++ b/tableauserverclient/helpers/headers.py @@ -2,18 +2,21 @@ from typing import Any, Generic, Mapping, Optional, TypeVar, Union from urllib.parse import unquote_plus -T = TypeVar("T", ) +T = TypeVar( + "T", +) + def fix_filename(params: Mapping[str, T]) -> Mapping[str, T]: if "filename*" not in params: return params - + params = deepcopy(params) filename = params["filename*"] prefix = "UTF-8''" if filename.startswith(prefix): - filename = filename[len(prefix):] + filename = filename[len(prefix) :] params["filename"] = unquote_plus(filename) del params["filename*"] - return params \ No newline at end of file + return params diff --git a/test/test_datasource.py b/test/test_datasource.py index c79bf45fd..f258fdc52 100644 --- a/test/test_datasource.py +++ b/test/test_datasource.py @@ -703,10 +703,7 @@ def test_bad_download_response(self) -> None: self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", headers={ "Content-Disposition": '''name="tableau_datasource"; filename*=UTF-8''"Sample datasource.tds"''' - } - ) - file_path = self.server.datasources.download( - "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", - td + }, ) - self.assertTrue(os.path.exists(file_path)) \ No newline at end of file + file_path = self.server.datasources.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", td) + self.assertTrue(os.path.exists(file_path)) diff --git a/test/test_flow.py b/test/test_flow.py index d7fa2dbc3..a90b18171 100644 --- a/test/test_flow.py +++ b/test/test_flow.py @@ -209,12 +209,7 @@ def test_bad_download_response(self) -> None: with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: m.get( self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", - headers={ - "Content-Disposition": '''name="tableau_flow"; filename*=UTF-8''"Sample flow.tfl"''' - } - ) - file_path = self.server.flows.download( - "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", - td + headers={"Content-Disposition": '''name="tableau_flow"; filename*=UTF-8''"Sample flow.tfl"'''}, ) + file_path = self.server.flows.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", td) self.assertTrue(os.path.exists(file_path)) diff --git a/test/test_workbook.py b/test/test_workbook.py index 9804b2c02..212d55a37 100644 --- a/test/test_workbook.py +++ b/test/test_workbook.py @@ -937,12 +937,7 @@ def test_bad_download_response(self) -> None: with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: m.get( self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", - headers={ - "Content-Disposition": '''name="tableau_workbook"; filename*=UTF-8''"Sample workbook.twb"''' - } - ) - file_path = self.server.workbooks.download( - "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", - td + headers={"Content-Disposition": '''name="tableau_workbook"; filename*=UTF-8''"Sample workbook.twb"'''}, ) + file_path = self.server.workbooks.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", td) self.assertTrue(os.path.exists(file_path)) From 19a9f51ab7ab65a1819bfabf28f885d2fe0df7e2 Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Sat, 9 Dec 2023 22:12:15 -0600 Subject: [PATCH 4/4] fix: strip typing from fix_filename --- tableauserverclient/helpers/headers.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tableauserverclient/helpers/headers.py b/tableauserverclient/helpers/headers.py index 57be21b23..2ed4a814d 100644 --- a/tableauserverclient/helpers/headers.py +++ b/tableauserverclient/helpers/headers.py @@ -1,13 +1,8 @@ from copy import deepcopy -from typing import Any, Generic, Mapping, Optional, TypeVar, Union from urllib.parse import unquote_plus -T = TypeVar( - "T", -) - -def fix_filename(params: Mapping[str, T]) -> Mapping[str, T]: +def fix_filename(params): if "filename*" not in params: return params