Skip to content

Commit 21b2cdd

Browse files
jacalataclaude
andcommitted
feat: add webhook update method and isEnabled/statusChangeReason fields
Fixes #1135 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a70be81 commit 21b2cdd

5 files changed

Lines changed: 137 additions & 8 deletions

File tree

tableauserverclient/models/webhook_item.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ class WebhookItem:
4444
4545
owner_id : str | None
4646
The identifier (luid) of the user who owns the webhook.
47+
48+
is_enabled : bool | None
49+
Whether the webhook is enabled. Disabled webhooks do not fire.
50+
51+
status_change_reason : str | None
52+
The reason the webhook status last changed (e.g. why it was disabled).
4753
"""
4854

4955
def __init__(self):
@@ -52,8 +58,10 @@ def __init__(self):
5258
self.url: str | None = None
5359
self._event: str | None = None
5460
self.owner_id: str | None = None
61+
self.is_enabled: bool | None = None
62+
self.status_change_reason: str | None = None
5563

56-
def _set_values(self, id, name, url, event, owner_id):
64+
def _set_values(self, id, name, url, event, owner_id, is_enabled=None, status_change_reason=None):
5765
if id is not None:
5866
self._id = id
5967
if name:
@@ -64,6 +72,10 @@ def _set_values(self, id, name, url, event, owner_id):
6472
self.event = event
6573
if owner_id:
6674
self.owner_id = owner_id
75+
if is_enabled is not None:
76+
self.is_enabled = is_enabled
77+
if status_change_reason is not None:
78+
self.status_change_reason = status_change_reason
6779

6880
@property
6981
def id(self) -> str | None:
@@ -116,7 +128,14 @@ def _parse_element(webhook_xml: ET.Element, ns) -> tuple:
116128
if owner_tag is not None:
117129
owner_id = owner_tag.get("id", None)
118130

119-
return id, name, url, event, owner_id
131+
is_enabled = None
132+
is_enabled_str = webhook_xml.get("isEnabled", None)
133+
if is_enabled_str is not None:
134+
is_enabled = is_enabled_str.lower() == "true"
135+
136+
status_change_reason = webhook_xml.get("statusChangeReason", None)
137+
138+
return id, name, url, event, owner_id, is_enabled, status_change_reason
120139

121140
def __repr__(self) -> str:
122-
return f"<Webhook id={self.id} name={self.name} url={self.url} event={self.event}>"
141+
return f"<Webhook id={self.id} name={self.name} url={self.url} event={self.event} is_enabled={self.is_enabled}>"

tableauserverclient/server/endpoint/webhooks_endpoint.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22

33
from .endpoint import Endpoint, api
4+
from .exceptions import MissingRequiredFieldError
45
from tableauserverclient.server import RequestFactory
56
from tableauserverclient.models import WebhookItem, PaginationItem
67

@@ -118,6 +119,33 @@ def create(self, webhook_item: WebhookItem) -> WebhookItem:
118119
logger.info(f"Created new webhook (ID: {new_webhook.id})")
119120
return new_webhook
120121

122+
@api(version="3.6")
123+
def update(self, webhook_item: WebhookItem) -> WebhookItem:
124+
"""
125+
Modifies an existing webhook.
126+
127+
REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#update_webhook
128+
129+
Parameters
130+
----------
131+
webhook_item : WebhookItem
132+
The webhook item to update. Must have a valid id.
133+
134+
Returns
135+
-------
136+
WebhookItem
137+
An object containing information about the updated webhook.
138+
"""
139+
if not webhook_item.id:
140+
error = "Webhook item missing ID. Webhook must be retrieved from server first."
141+
raise MissingRequiredFieldError(error)
142+
url = f"{self.baseurl}/{webhook_item.id}"
143+
update_req = RequestFactory.Webhook.update_req(webhook_item)
144+
server_response = self.put_request(url, update_req)
145+
updated_webhook = WebhookItem.from_response(server_response.content, self.parent_srv.namespace)[0]
146+
logger.info(f"Updated webhook (ID: {webhook_item.id})")
147+
return updated_webhook
148+
121149
@api(version="3.6")
122150
def test(self, webhook_id: str):
123151
"""

tableauserverclient/server/request_factory.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,6 +1412,26 @@ def create_req(self, xml_request: ET.Element, webhook_item: "WebhookItem") -> by
14121412

14131413
return ET.tostring(xml_request)
14141414

1415+
@_tsrequest_wrapped
1416+
def update_req(self, xml_request: ET.Element, webhook_item: "WebhookItem") -> bytes:
1417+
webhook = ET.SubElement(xml_request, "webhook")
1418+
if webhook_item.name is not None:
1419+
webhook.attrib["name"] = webhook_item.name
1420+
if webhook_item.is_enabled is not None:
1421+
webhook.attrib["isEnabled"] = str(webhook_item.is_enabled).lower()
1422+
1423+
if webhook_item._event is not None:
1424+
source = ET.SubElement(webhook, "webhook-source")
1425+
ET.SubElement(source, webhook_item._event)
1426+
1427+
if webhook_item.url is not None:
1428+
destination = ET.SubElement(webhook, "webhook-destination")
1429+
post = ET.SubElement(destination, "webhook-destination-http")
1430+
post.attrib["method"] = "POST"
1431+
post.attrib["url"] = webhook_item.url
1432+
1433+
return ET.tostring(xml_request)
1434+
14151435

14161436
class MetricRequest:
14171437
@_tsrequest_wrapped

test/assets/webhook_update.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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 http://tableau.com/api/ts-api-2.3.xsd">
3+
<webhook id="webhook-id" name="webhook-name-updated" isEnabled="true" statusChangeReason="">
4+
<webhook-source>
5+
<webhook-source-event-datasource-created />
6+
</webhook-source>
7+
<webhook-destination>
8+
<webhook-destination-http method="POST" url="https://updated-url.example.com/hook"/>
9+
</webhook-destination>
10+
<owner id="webhook_owner_luid" name="webhook_owner_name"/>
11+
</webhook>
12+
</tsResponse>

test/test_webhook.py

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
GET_NEW_EVENT_XML = TEST_ASSET_DIR / "webhook_get_new_event.xml"
1414
CREATE_XML = TEST_ASSET_DIR / "webhook_create.xml"
1515
CREATE_REQUEST_XML = TEST_ASSET_DIR / "webhook_create_request.xml"
16+
UPDATE_XML = TEST_ASSET_DIR / "webhook_update.xml"
1617

1718

1819
@pytest.fixture(scope="function")
@@ -90,8 +91,7 @@ def test_request_factory():
9091
assert webhook_request_expected.replace("\r", "") == webhook_request_actual
9192

9293

93-
def test_event_setter_none():
94-
"""Setting event to None should store None without crashing."""
94+
def test_event_setter_none() -> None:
9595
item = WebhookItem()
9696
item.event = "datasource-updated"
9797
assert item.event == "datasource-updated"
@@ -100,23 +100,23 @@ def test_event_setter_none():
100100
assert item.event is None
101101

102102

103-
def test_event_setter_short_name():
103+
def test_event_setter_short_name() -> None:
104104
"""Short event names should be stored with the webhook-source-event- prefix."""
105105
item = WebhookItem()
106106
item.event = "datasource-updated"
107107
assert item._event == "webhook-source-event-datasource-updated"
108108
assert item.event == "datasource-updated"
109109

110110

111-
def test_event_setter_full_source_name():
111+
def test_event_setter_full_source_name() -> None:
112112
"""Full webhook-source-event- names should be accepted and stored as-is."""
113113
item = WebhookItem()
114114
item.event = "webhook-source-event-datasource-updated"
115115
assert item._event == "webhook-source-event-datasource-updated"
116116
assert item.event == "datasource-updated"
117117

118118

119-
def test_event_setter_new_style_event_name():
119+
def test_event_setter_new_style_event_name() -> None:
120120
"""New-style event names (webhook-event-*) should be stored as-is and not mangled."""
121121
item = WebhookItem()
122122
item.event = "webhook-event-user-promoted-admin"
@@ -167,3 +167,53 @@ def test_create_with_source_event_name(server: TSC.Server) -> None:
167167

168168
new_webhook = server.webhooks.create(webhook_model)
169169
assert new_webhook.id is not None
170+
171+
172+
def test_get_parses_is_enabled_and_status_change_reason(server: TSC.Server) -> None:
173+
response_xml = UPDATE_XML.read_text()
174+
with requests_mock.mock() as m:
175+
m.get(server.webhooks.baseurl + "/webhook-id", text=response_xml)
176+
webhook = server.webhooks.get_by_id("webhook-id")
177+
178+
assert webhook.is_enabled is True
179+
assert webhook.status_change_reason == ""
180+
assert webhook.name == "webhook-name-updated"
181+
assert webhook.url == "https://updated-url.example.com/hook"
182+
183+
184+
def test_update(server: TSC.Server) -> None:
185+
response_xml = UPDATE_XML.read_text()
186+
with requests_mock.mock() as m:
187+
m.put(server.webhooks.baseurl + "/webhook-id", text=response_xml)
188+
webhook_item = WebhookItem()
189+
webhook_item._set_values(
190+
"webhook-id", "webhook-name-updated", "https://updated-url.example.com/hook", "datasource-created", None
191+
)
192+
webhook_item.is_enabled = True
193+
194+
updated_webhook = server.webhooks.update(webhook_item)
195+
196+
assert updated_webhook.id == "webhook-id"
197+
assert updated_webhook.name == "webhook-name-updated"
198+
assert updated_webhook.url == "https://updated-url.example.com/hook"
199+
assert updated_webhook.is_enabled is True
200+
201+
202+
def test_update_missing_id(server: TSC.Server) -> None:
203+
webhook_item = WebhookItem()
204+
webhook_item.name = "some-webhook"
205+
with pytest.raises(Exception):
206+
server.webhooks.update(webhook_item)
207+
208+
209+
def test_update_request_factory_is_enabled() -> None:
210+
webhook_item = WebhookItem()
211+
webhook_item._set_values(
212+
"webhook-id", "webhook-name", "https://example.com/hook", "datasource-created", None, is_enabled=False
213+
)
214+
215+
request_bytes = RequestFactory.Webhook.update_req(webhook_item)
216+
request_str = request_bytes.decode("utf-8")
217+
218+
assert 'isEnabled="false"' in request_str
219+
assert "webhook-name" in request_str

0 commit comments

Comments
 (0)