Skip to content

Commit d3dbd7f

Browse files
Merge branch 'development' into fix-update-connections-optional-auth
2 parents b8e2aba + ec0614e commit d3dbd7f

10 files changed

Lines changed: 154 additions & 13 deletions

tableauserverclient/server/endpoint/custom_views_endpoint.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def populate_image(self, view_item: CustomViewItem, req_options: Optional["Image
121121
view_item : CustomViewItem
122122
123123
req_options : ImageRequestOptions, optional
124-
Options to customize the image returned, by default None
124+
Options to customize the image returned, including format (PNG or SVG), by default None
125125
126126
Returns
127127
-------
@@ -139,6 +139,13 @@ def populate_image(self, view_item: CustomViewItem, req_options: Optional["Image
139139
def image_fetcher():
140140
return self._get_view_image(view_item, req_options)
141141

142+
if req_options is not None:
143+
if not self.parent_srv.check_at_least_version("3.29"):
144+
if req_options.format:
145+
from tableauserverclient.server.endpoint.exceptions import UnsupportedAttributeError
146+
147+
raise UnsupportedAttributeError("format parameter is only supported in 3.29+")
148+
142149
view_item._set_image(image_fetcher)
143150
logger.info(f"Populated image for custom view (ID: {view_item.id})")
144151

tableauserverclient/server/endpoint/schedules_endpoint.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,12 +298,12 @@ def batch_update_state(
298298
@api(version="3.27")
299299
def batch_update_state(self, schedules, state, update_all=False) -> list[str]:
300300
"""
301-
Batch update the status of one or more scheudles. If update_all is set,
301+
Batch update the status of one or more schedules. If update_all is set,
302302
all schedules on the Tableau Server are affected.
303303
304304
Parameters
305305
----------
306-
schedules: Iterable[ScheudleItem | str] | Any
306+
schedules: Iterable[ScheduleItem | str] | Any
307307
The schedules to be updated. If update_all=True, this is ignored.
308308
309309
state: Literal["active", "suspended"]

tableauserverclient/server/endpoint/views_endpoint.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def populate_image(self, view_item: ViewItem, req_options: Optional["ImageReques
158158
159159
req_options: Optional[ImageRequestOptions], default None
160160
Optional request options for the request. These options can include
161-
parameters such as image resolution and max age.
161+
parameters such as image resolution, max age, and format (PNG or SVG).
162162
163163
Returns
164164
-------
@@ -171,9 +171,13 @@ def populate_image(self, view_item: ViewItem, req_options: Optional["ImageReques
171171
def image_fetcher():
172172
return self._get_view_image(view_item, req_options)
173173

174-
if not self.parent_srv.check_at_least_version("3.23") and req_options is not None:
175-
if req_options.viz_height or req_options.viz_width:
176-
raise UnsupportedAttributeError("viz_height and viz_width are only supported in 3.23+")
174+
if req_options is not None:
175+
if not self.parent_srv.check_at_least_version("3.23"):
176+
if req_options.viz_height or req_options.viz_width:
177+
raise UnsupportedAttributeError("viz_height and viz_width are only supported in 3.23+")
178+
if not self.parent_srv.check_at_least_version("3.29"):
179+
if req_options.format:
180+
raise UnsupportedAttributeError("format parameter is only supported in 3.29+")
177181

178182
view_item._set_image(image_fetcher)
179183
logger.info(f"Populated image for view (ID: {view_item.id})")

tableauserverclient/server/endpoint/workbooks_endpoint.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from tableauserverclient.server.endpoint.exceptions import (
1515
InternalServerError,
1616
MissingRequiredFieldError,
17+
ServerResponseError,
1718
UnsupportedAttributeError,
1819
)
1920
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
@@ -125,7 +126,7 @@ def get_by_id(self, workbook_id: str) -> WorkbookItem:
125126
return WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)[0]
126127

127128
@api(version="2.8")
128-
def refresh(self, workbook_item: Union[WorkbookItem, str], incremental: bool = False) -> JobItem:
129+
def refresh(self, workbook_item: Union[WorkbookItem, str], incremental: bool = False) -> JobItem | None:
129130
"""
130131
Refreshes the extract of an existing workbook.
131132
@@ -138,13 +139,19 @@ def refresh(self, workbook_item: Union[WorkbookItem, str], incremental: bool = F
138139
139140
Returns
140141
-------
141-
JobItem
142-
The job item.
142+
JobItem | None
143+
The job item, or None if a refresh job is already queued for this workbook.
143144
"""
144145
id_ = getattr(workbook_item, "id", workbook_item)
145146
url = f"{self.baseurl}/{id_}/refresh"
146147
refresh_req = RequestFactory.Task.refresh_req(incremental, self.parent_srv)
147-
server_response = self.post_request(url, refresh_req)
148+
try:
149+
server_response = self.post_request(url, refresh_req)
150+
except ServerResponseError as e:
151+
if e.code.startswith("409") and "already" in e.detail:
152+
logger.warning(f"{e.summary} {e.detail}")
153+
return None
154+
raise
148155
new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
149156
return new_job
150157

tableauserverclient/server/request_options.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,10 @@ class ImageRequestOptions(_ImagePDFCommonExportOptions):
497497
viz_width: int, optional
498498
The width of the viz in pixels. If specified, viz_height must also be specified.
499499
500+
format: str, optional
501+
The format of the image to export. Use Format.PNG, Format.SVG, Format.png, or Format.svg.
502+
Default is "PNG". Available in API version 3.29+.
503+
500504
"""
501505

502506
extension = "png"
@@ -505,14 +509,21 @@ class ImageRequestOptions(_ImagePDFCommonExportOptions):
505509
class Resolution:
506510
High = "high"
507511

508-
def __init__(self, imageresolution=None, maxage=-1, viz_height=None, viz_width=None):
512+
class Format:
513+
PNG = "PNG"
514+
SVG = "SVG"
515+
516+
def __init__(self, imageresolution=None, maxage=-1, viz_height=None, viz_width=None, format=None):
509517
super().__init__(maxage=maxage, viz_height=viz_height, viz_width=viz_width)
510518
self.image_resolution = imageresolution
519+
self.format = format
511520

512521
def get_query_params(self):
513522
params = super().get_query_params()
514523
if self.image_resolution:
515524
params["resolution"] = self.image_resolution
525+
if self.format:
526+
params["format"] = self.format
516527
return params
517528

518529

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api https://help.tableau.com/samples/en-us/rest_api/ts-api_3_27.xsd">
3+
<error code="409093"><summary>Resource Conflict</summary><detail>Job for \'extract\' is already queued. Not queuing a duplicate.</detail></error></tsResponse>

test/test_custom_view.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,54 @@ def test_populate_image_with_options(server: TSC.Server) -> None:
116116
assert response == single_view.image
117117

118118

119+
def test_populate_image_svg_format(server: TSC.Server) -> None:
120+
server.version = "3.29"
121+
response = b"<svg>test</svg>"
122+
with requests_mock.mock() as m:
123+
m.get(
124+
server.custom_views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?format=SVG",
125+
content=response,
126+
)
127+
single_view = TSC.CustomViewItem()
128+
single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5"
129+
req_option = TSC.ImageRequestOptions(format=TSC.ImageRequestOptions.Format.SVG)
130+
server.custom_views.populate_image(single_view, req_option)
131+
assert response == single_view.image
132+
133+
134+
def test_populate_image_png_format(server: TSC.Server) -> None:
135+
server.version = "3.29"
136+
response = POPULATE_PREVIEW_IMAGE.read_bytes()
137+
with requests_mock.mock() as m:
138+
m.get(
139+
server.custom_views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?format=PNG",
140+
content=response,
141+
)
142+
single_view = TSC.CustomViewItem()
143+
single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5"
144+
req_option = TSC.ImageRequestOptions(format=TSC.ImageRequestOptions.Format.PNG)
145+
server.custom_views.populate_image(single_view, req_option)
146+
assert response == single_view.image
147+
148+
149+
def test_populate_image_format_unsupported_version(server: TSC.Server) -> None:
150+
from tableauserverclient.server.endpoint.exceptions import UnsupportedAttributeError
151+
152+
server.version = "3.28"
153+
response = POPULATE_PREVIEW_IMAGE.read_bytes()
154+
with requests_mock.mock() as m:
155+
m.get(
156+
server.custom_views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?format=SVG",
157+
content=response,
158+
)
159+
single_view = TSC.CustomViewItem()
160+
single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5"
161+
req_option = TSC.ImageRequestOptions(format=TSC.ImageRequestOptions.Format.SVG)
162+
163+
with pytest.raises(UnsupportedAttributeError):
164+
server.custom_views.populate_image(single_view, req_option)
165+
166+
119167
def test_populate_image_missing_id(server: TSC.Server) -> None:
120168
single_view = TSC.CustomViewItem()
121169
single_view._id = None

test/test_database.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def test_populate_data_quality_warning(server):
9898
first_dqw = dqws.pop()
9999
assert first_dqw.id == "c2e0e406-84fb-4f4e-9998-f20dd9306710"
100100
assert first_dqw.warning_type == "WARNING"
101-
assert first_dqw.message, "Hello == World!"
101+
assert first_dqw.message == "Hello, World!"
102102
assert first_dqw.owner_id == "eddc8c5f-6af0-40be-b6b0-2c790290a43f"
103103
assert first_dqw.active
104104
assert first_dqw.severe

test/test_view.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,52 @@ def test_populate_image_with_options(server: TSC.Server) -> None:
238238
assert response == single_view.image
239239

240240

241+
def test_populate_image_svg_format(server: TSC.Server) -> None:
242+
server.version = "3.29"
243+
response = b"<svg>test</svg>"
244+
with requests_mock.mock() as m:
245+
m.get(
246+
server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?format=SVG",
247+
content=response,
248+
)
249+
single_view = TSC.ViewItem()
250+
single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5"
251+
req_option = TSC.ImageRequestOptions(format=TSC.ImageRequestOptions.Format.SVG)
252+
server.views.populate_image(single_view, req_option)
253+
assert response == single_view.image
254+
255+
256+
def test_populate_image_png_format(server: TSC.Server) -> None:
257+
server.version = "3.29"
258+
response = POPULATE_PREVIEW_IMAGE.read_bytes()
259+
with requests_mock.mock() as m:
260+
m.get(
261+
server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?format=PNG",
262+
content=response,
263+
)
264+
single_view = TSC.ViewItem()
265+
single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5"
266+
req_option = TSC.ImageRequestOptions(format=TSC.ImageRequestOptions.Format.PNG)
267+
server.views.populate_image(single_view, req_option)
268+
assert response == single_view.image
269+
270+
271+
def test_populate_image_format_unsupported_version(server: TSC.Server) -> None:
272+
server.version = "3.28"
273+
response = POPULATE_PREVIEW_IMAGE.read_bytes()
274+
with requests_mock.mock() as m:
275+
m.get(
276+
server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?format=SVG",
277+
content=response,
278+
)
279+
single_view = TSC.ViewItem()
280+
single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5"
281+
req_option = TSC.ImageRequestOptions(format=TSC.ImageRequestOptions.Format.SVG)
282+
283+
with pytest.raises(UnsupportedAttributeError):
284+
server.views.populate_image(single_view, req_option)
285+
286+
241287
def test_populate_pdf(server: TSC.Server) -> None:
242288
response = POPULATE_PDF.read_bytes()
243289
with requests_mock.mock() as m:

test/test_workbook.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
PUBLISH_XML = TEST_ASSET_DIR / "workbook_publish.xml"
3535
PUBLISH_ASYNC_XML = TEST_ASSET_DIR / "workbook_publish_async.xml"
3636
REFRESH_XML = TEST_ASSET_DIR / "workbook_refresh.xml"
37+
WORKBOOK_REFRESH_DUPLICATE_XML = TEST_ASSET_DIR / "workbook_refresh_duplicate.xml"
3738
REVISION_XML = TEST_ASSET_DIR / "workbook_revision.xml"
3839
UPDATE_XML = TEST_ASSET_DIR / "workbook_update.xml"
3940
UPDATE_PERMISSIONS = TEST_ASSET_DIR / "workbook_update_permissions.xml"
@@ -179,6 +180,20 @@ def test_refresh_id(server: TSC.Server) -> None:
179180
server.workbooks.refresh("3cc6cd06-89ce-4fdc-b935-5294135d6d42")
180181

181182

183+
def test_refresh_already_running(server: TSC.Server) -> None:
184+
server.version = "2.8"
185+
server.workbooks.baseurl
186+
response_xml = WORKBOOK_REFRESH_DUPLICATE_XML.read_text()
187+
with requests_mock.mock() as m:
188+
m.post(
189+
server.workbooks.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/refresh",
190+
status_code=409,
191+
text=response_xml,
192+
)
193+
refresh_job = server.workbooks.refresh("3cc6cd06-89ce-4fdc-b935-5294135d6d42")
194+
assert refresh_job is None
195+
196+
182197
def test_refresh_object(server: TSC.Server) -> None:
183198
server.version = "2.8"
184199
server.workbooks.baseurl

0 commit comments

Comments
 (0)