Skip to content

Commit 6eb267d

Browse files
fix: include all supported fields for ad-hoc webhooks (#855)
This PR updates the auto-generated Pydantic models and TypedDicts based on OpenAPI specification changes in [apify-docs PR #2640](apify/apify-docs#2640). On top of the regenerated models, it fixes a related bug: ad-hoc webhooks passed to `start()` / `call()` silently dropped `idempotency_key`, `ignore_ssl_errors` and `do_not_retry`. `WebhookRepresentation` declared only the four templating fields, and `encode_webhooks_to_base64` keeps only declared fields, so these three never reached the platform. The platform accepts and honors all three for ad-hoc webhooks (verified in apify-core: the run-start handler feeds them into the same webhook-creation path as the create-webhook endpoint, including the `idempotencyKey` dedup). What changed: - The regenerated models add the three fields to `WebhookRepresentation` and both webhook-representation typed dicts (snake_case + camelCase). - Reworks the `WebhookCreate` / dict projection in `encode_webhooks_to_base64` to filter by the fields `WebhookRepresentation` declares, instead of hand-listing them. New ad-hoc fields now flow through automatically, so this class of silent-drop bug cannot recur. Unblocks apify/apify-sdk-python#963, which forwards these fields from the SDK `Webhook` dataclass. --------- Co-authored-by: Vlada Dusek <v.dusek96@gmail.com>
1 parent 68d74c1 commit 6eb267d

4 files changed

Lines changed: 119 additions & 16 deletions

File tree

src/apify_client/_models.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3766,6 +3766,22 @@ class WebhookRepresentation(BaseModel):
37663766
"""
37673767
Optional template for the HTTP headers sent by the webhook.
37683768
"""
3769+
should_interpolate_strings: Annotated[bool | None, Field(alias='shouldInterpolateStrings', examples=[False])] = None
3770+
"""
3771+
Flag to also interpolate `{{...}}` variables inside string values of the payload and headers templates.
3772+
"""
3773+
idempotency_key: Annotated[str | None, Field(alias='idempotencyKey', examples=['fdSJmdP3nfs7sfk3y'])] = None
3774+
"""
3775+
Key that prevents creating duplicate webhooks, e.g. when the run-starting request is retried.
3776+
"""
3777+
ignore_ssl_errors: Annotated[bool | None, Field(alias='ignoreSslErrors', examples=[False])] = None
3778+
"""
3779+
Flag to ignore SSL errors when the webhook sends the request.
3780+
"""
3781+
do_not_retry: Annotated[bool | None, Field(alias='doNotRetry', examples=[False])] = None
3782+
"""
3783+
Flag to skip retrying the webhook request on failure.
3784+
"""
37693785

37703786

37713787
@docs_group('Models')

src/apify_client/_typeddicts.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,22 @@ class WebhookRepresentationDict(TypedDict):
336336
"""
337337
Optional template for the HTTP headers sent by the webhook.
338338
"""
339+
should_interpolate_strings: NotRequired[bool | None]
340+
"""
341+
Flag to also interpolate `{{...}}` variables inside string values of the payload and headers templates.
342+
"""
343+
idempotency_key: NotRequired[str | None]
344+
"""
345+
Key that prevents creating duplicate webhooks, e.g. when the run-starting request is retried.
346+
"""
347+
ignore_ssl_errors: NotRequired[bool | None]
348+
"""
349+
Flag to ignore SSL errors when the webhook sends the request.
350+
"""
351+
do_not_retry: NotRequired[bool | None]
352+
"""
353+
Flag to skip retrying the webhook request on failure.
354+
"""
339355

340356

341357
@docs_group('Typed dicts')
@@ -374,3 +390,19 @@ class WebhookRepresentationCamelDict(TypedDict):
374390
"""
375391
Optional template for the HTTP headers sent by the webhook.
376392
"""
393+
shouldInterpolateStrings: NotRequired[bool | None]
394+
"""
395+
Flag to also interpolate `{{...}}` variables inside string values of the payload and headers templates.
396+
"""
397+
idempotencyKey: NotRequired[str | None]
398+
"""
399+
Key that prevents creating duplicate webhooks, e.g. when the run-starting request is retried.
400+
"""
401+
ignoreSslErrors: NotRequired[bool | None]
402+
"""
403+
Flag to ignore SSL errors when the webhook sends the request.
404+
"""
405+
doNotRetry: NotRequired[bool | None]
406+
"""
407+
Flag to skip retrying the webhook request on failure.
408+
"""

src/apify_client/_utils.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -288,31 +288,25 @@ def encode_webhooks_to_base64(webhooks: WebhooksList | None) -> str | None:
288288
289289
Returns `None` for `None` or an empty list, so the query parameter is omitted.
290290
291-
See `WebhooksList` for the accepted shapes. `WebhookRepresentation` instances are used as-is; `WebhookCreate`
292-
instances are projected onto the `WebhookRepresentation` fields, dropping persistent-only fields like `condition`.
293-
Dict shapes are validated into `WebhookRepresentation` and only fields it declares are kept.
291+
See `WebhooksList` for the accepted shapes. `WebhookRepresentation` instances are used as-is. `WebhookCreate`
292+
instances and dict shapes are projected onto the fields `WebhookRepresentation` declares, dropping anything else
293+
(e.g. persistent-only fields like `condition`). Filtering by the declared field names and aliases means new
294+
ad-hoc fields added to `WebhookRepresentation` flow through automatically, without touching this function.
294295
"""
295296
if not webhooks:
296297
return None
297298

298299
representations = list[WebhookRepresentation]()
300+
allowed = _webhook_representation_keys()
299301

300302
for webhook in webhooks:
301303
if isinstance(webhook, WebhookRepresentation):
302304
representations.append(webhook)
303-
elif isinstance(webhook, WebhookCreate):
304-
representations.append(
305-
WebhookRepresentation(
306-
event_types=webhook.event_types,
307-
request_url=webhook.request_url,
308-
payload_template=webhook.payload_template,
309-
headers_template=webhook.headers_template,
310-
)
311-
)
312-
else:
313-
allowed = _webhook_representation_keys()
314-
filtered = {k: v for k, v in webhook.items() if k in allowed}
315-
representations.append(WebhookRepresentation.model_validate(filtered))
305+
continue
306+
307+
data = webhook.model_dump(by_alias=True) if isinstance(webhook, WebhookCreate) else dict(webhook)
308+
filtered = {key: value for key, value in data.items() if key in allowed}
309+
representations.append(WebhookRepresentation.model_validate(filtered))
316310

317311
data = [r.model_dump(by_alias=True, exclude_none=True) for r in representations]
318312
json_string = json.dumps(data).encode(encoding='utf-8')

tests/unit/test_utils.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from __future__ import annotations
22

33
import io
4+
import json
5+
from base64 import b64decode
46
from datetime import timedelta
57
from http import HTTPStatus
68
from typing import TYPE_CHECKING
@@ -27,6 +29,7 @@
2729

2830
if TYPE_CHECKING:
2931
from apify_client._typeddicts import WebhookRepresentationDict
32+
from apify_client.types import WebhooksList
3033

3134

3235
def test_to_safe_id() -> None:
@@ -58,6 +61,64 @@ def test_encode_webhooks_to_base64() -> None:
5861
)
5962

6063

64+
@pytest.mark.parametrize(
65+
'webhooks',
66+
[
67+
pytest.param(
68+
[
69+
WebhookCreate(
70+
event_types=['ACTOR.RUN.SUCCEEDED'],
71+
condition=WebhookCondition(),
72+
request_url='https://example.com/run-succeeded',
73+
idempotency_key='some-key',
74+
ignore_ssl_errors=True,
75+
do_not_retry=True,
76+
),
77+
],
78+
id='webhook-create-model',
79+
),
80+
pytest.param(
81+
[
82+
{
83+
'event_types': ['ACTOR.RUN.SUCCEEDED'],
84+
'request_url': 'https://example.com/run-succeeded',
85+
'idempotency_key': 'some-key',
86+
'ignore_ssl_errors': True,
87+
'do_not_retry': True,
88+
},
89+
],
90+
id='snake-case-dict',
91+
),
92+
pytest.param(
93+
[
94+
{
95+
'eventTypes': ['ACTOR.RUN.SUCCEEDED'],
96+
'requestUrl': 'https://example.com/run-succeeded',
97+
'idempotencyKey': 'some-key',
98+
'ignoreSslErrors': True,
99+
'doNotRetry': True,
100+
},
101+
],
102+
id='camel-case-dict',
103+
),
104+
],
105+
)
106+
def test_encode_webhooks_to_base64_keeps_adhoc_fields(webhooks: WebhooksList) -> None:
107+
"""Test that the idempotency key and the SSL/retry flags survive the projection for every accepted shape."""
108+
result = encode_webhooks_to_base64(webhooks)
109+
110+
assert result is not None
111+
assert json.loads(b64decode(result)) == [
112+
{
113+
'eventTypes': ['ACTOR.RUN.SUCCEEDED'],
114+
'requestUrl': 'https://example.com/run-succeeded',
115+
'idempotencyKey': 'some-key',
116+
'ignoreSslErrors': True,
117+
'doNotRetry': True,
118+
}
119+
]
120+
121+
61122
def test_encode_webhooks_to_base64_from_dicts() -> None:
62123
"""Test that encode_webhooks_to_base64 accepts plain dicts typed as WebhookRepresentationDict."""
63124
webhooks: list[WebhookRepresentationDict] = [

0 commit comments

Comments
 (0)