From 75d8748305788dbefa8d455cb0cc1d5c40d90962 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 11 Jun 2026 14:48:53 +0200 Subject: [PATCH 1/7] fix: warn when ad-hoc webhooks drop unsupported Webhook fields --- src/apify/_webhook.py | 30 +++++++++++++++++++---- tests/unit/actor/test_actor_helpers.py | 33 ++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/apify/_webhook.py b/src/apify/_webhook.py index 129385c9..6e7be23d 100644 --- a/src/apify/_webhook.py +++ b/src/apify/_webhook.py @@ -7,6 +7,7 @@ from crawlee._utils.urls import validate_http_url from apify._utils import docs_group +from apify.log import logger if TYPE_CHECKING: from apify_client._literals import WebhookEventType @@ -19,6 +20,9 @@ class Webhook: The same instance can be passed as an ad-hoc webhook to `Actor.start()` / `Actor.call()` or as a persistent webhook to `Actor.add_webhook()` (the `condition.actor_run_id` is set automatically to the current run). + + Ad-hoc webhooks support only `event_types`, `request_url`, `payload_template` and `headers_template`; the + remaining fields apply only to `Actor.add_webhook()` and are ignored (with a warning) otherwise. """ event_types: list[WebhookEventType] @@ -34,13 +38,13 @@ class Webhook: """Template for the HTTP headers sent by the webhook.""" idempotency_key: str | None = None - """Key that prevents creating duplicate webhooks.""" + """Key that prevents creating duplicate webhooks. Only applies to `Actor.add_webhook()`.""" ignore_ssl_errors: bool | None = None - """Whether to ignore SSL errors when sending the request.""" + """Whether to ignore SSL errors when sending the request. Only applies to `Actor.add_webhook()`.""" do_not_retry: bool | None = None - """Whether to skip retrying the request on failure.""" + """Whether to skip retrying the request on failure. Only applies to `Actor.add_webhook()`.""" def __post_init__(self) -> None: # Fail fast on a malformed URL at construction time instead of deferring the error to the API call. @@ -48,9 +52,27 @@ def __post_init__(self) -> None: def to_client_representations(webhooks: list[Webhook] | None) -> list[WebhookRepresentation] | None: - """Project SDK webhooks to the minimal ad-hoc representation accepted by the client's `start()` / `call()`.""" + """Project SDK webhooks to the minimal ad-hoc representation accepted by the client's `start()` / `call()`. + + Fields not supported by ad-hoc webhooks (`idempotency_key`, `ignore_ssl_errors`, `do_not_retry`) are dropped + with a warning. + """ if not webhooks: return None + + for webhook in webhooks: + dropped = [ + field + for field in ('idempotency_key', 'ignore_ssl_errors', 'do_not_retry') + if getattr(webhook, field) is not None + ] + if dropped: + fields = ', '.join(f'`{field}`' for field in dropped) + logger.warning( + f'Ad-hoc webhooks do not support {fields}; the field(s) will be ignored. ' + f'Use `Actor.add_webhook()` to create a webhook with them.' + ) + return [ WebhookRepresentation( event_types=w.event_types, diff --git a/tests/unit/actor/test_actor_helpers.py b/tests/unit/actor/test_actor_helpers.py index 5d943904..a7a683ad 100644 --- a/tests/unit/actor/test_actor_helpers.py +++ b/tests/unit/actor/test_actor_helpers.py @@ -252,6 +252,39 @@ async def test_remote_method_with_webhooks( assert kwargs['webhooks'] is not None +@pytest.mark.parametrize(('client_resource', 'client_method', 'actor_method_name', 'entity_id'), _ACTOR_REMOTE_METHODS) +async def test_remote_method_warns_on_unsupported_webhook_fields( + apify_client_async_patcher: ApifyClientAsyncPatcher, + fake_actor_run: Run, + client_resource: str, + client_method: str, + actor_method_name: str, + entity_id: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that start/call/call_task warn about `Webhook` fields not supported by ad-hoc webhooks.""" + apify_client_async_patcher.patch(client_resource, client_method, return_value=fake_actor_run) + caplog.set_level('WARNING') + + async with Actor: + actor_method = getattr(Actor, actor_method_name) + await actor_method( + entity_id, + webhooks=[ + Webhook( + event_types=['ACTOR.RUN.SUCCEEDED'], + request_url='https://example.com', + idempotency_key='some-key', + do_not_retry=True, + ) + ], + ) + + matching = [record for record in caplog.records if 'Ad-hoc webhooks do not support' in record.message] + assert len(matching) == 1 + assert '`idempotency_key`, `do_not_retry`' in matching[0].message + + @pytest.mark.parametrize(('client_resource', 'client_method', 'actor_method_name', 'entity_id'), _ACTOR_REMOTE_METHODS) async def test_remote_method_with_timedelta_timeout( apify_client_async_patcher: ApifyClientAsyncPatcher, From e9d4f4d794ef3e005352f7d9c93acd78f37cde9e Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 11 Jun 2026 15:08:59 +0200 Subject: [PATCH 2/7] fix: forward idempotency_key, ignore_ssl_errors and do_not_retry in ad-hoc webhooks --- src/apify/_webhook.py | 33 ++++++-------------------- tests/unit/actor/test_actor_helpers.py | 25 +++++++++++++------ 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/src/apify/_webhook.py b/src/apify/_webhook.py index 6e7be23d..2bc2dc99 100644 --- a/src/apify/_webhook.py +++ b/src/apify/_webhook.py @@ -7,7 +7,6 @@ from crawlee._utils.urls import validate_http_url from apify._utils import docs_group -from apify.log import logger if TYPE_CHECKING: from apify_client._literals import WebhookEventType @@ -20,9 +19,6 @@ class Webhook: The same instance can be passed as an ad-hoc webhook to `Actor.start()` / `Actor.call()` or as a persistent webhook to `Actor.add_webhook()` (the `condition.actor_run_id` is set automatically to the current run). - - Ad-hoc webhooks support only `event_types`, `request_url`, `payload_template` and `headers_template`; the - remaining fields apply only to `Actor.add_webhook()` and are ignored (with a warning) otherwise. """ event_types: list[WebhookEventType] @@ -38,13 +34,13 @@ class Webhook: """Template for the HTTP headers sent by the webhook.""" idempotency_key: str | None = None - """Key that prevents creating duplicate webhooks. Only applies to `Actor.add_webhook()`.""" + """Key that prevents creating duplicate webhooks.""" ignore_ssl_errors: bool | None = None - """Whether to ignore SSL errors when sending the request. Only applies to `Actor.add_webhook()`.""" + """Whether to ignore SSL errors when sending the request.""" do_not_retry: bool | None = None - """Whether to skip retrying the request on failure. Only applies to `Actor.add_webhook()`.""" + """Whether to skip retrying the request on failure.""" def __post_init__(self) -> None: # Fail fast on a malformed URL at construction time instead of deferring the error to the API call. @@ -52,33 +48,18 @@ def __post_init__(self) -> None: def to_client_representations(webhooks: list[Webhook] | None) -> list[WebhookRepresentation] | None: - """Project SDK webhooks to the minimal ad-hoc representation accepted by the client's `start()` / `call()`. - - Fields not supported by ad-hoc webhooks (`idempotency_key`, `ignore_ssl_errors`, `do_not_retry`) are dropped - with a warning. - """ + """Convert SDK webhooks to the ad-hoc representation accepted by the client's `start()` / `call()`.""" if not webhooks: return None - - for webhook in webhooks: - dropped = [ - field - for field in ('idempotency_key', 'ignore_ssl_errors', 'do_not_retry') - if getattr(webhook, field) is not None - ] - if dropped: - fields = ', '.join(f'`{field}`' for field in dropped) - logger.warning( - f'Ad-hoc webhooks do not support {fields}; the field(s) will be ignored. ' - f'Use `Actor.add_webhook()` to create a webhook with them.' - ) - return [ WebhookRepresentation( event_types=w.event_types, request_url=w.request_url, payload_template=w.payload_template, headers_template=w.headers_template, + idempotency_key=w.idempotency_key, + ignore_ssl_errors=w.ignore_ssl_errors, + do_not_retry=w.do_not_retry, ) for w in webhooks ] diff --git a/tests/unit/actor/test_actor_helpers.py b/tests/unit/actor/test_actor_helpers.py index a7a683ad..794dde44 100644 --- a/tests/unit/actor/test_actor_helpers.py +++ b/tests/unit/actor/test_actor_helpers.py @@ -253,18 +253,16 @@ async def test_remote_method_with_webhooks( @pytest.mark.parametrize(('client_resource', 'client_method', 'actor_method_name', 'entity_id'), _ACTOR_REMOTE_METHODS) -async def test_remote_method_warns_on_unsupported_webhook_fields( +async def test_remote_method_forwards_all_webhook_fields( apify_client_async_patcher: ApifyClientAsyncPatcher, fake_actor_run: Run, client_resource: str, client_method: str, actor_method_name: str, entity_id: str, - caplog: pytest.LogCaptureFixture, ) -> None: - """Test that start/call/call_task warn about `Webhook` fields not supported by ad-hoc webhooks.""" + """Test that start/call/call_task forward all `Webhook` fields to the client representation.""" apify_client_async_patcher.patch(client_resource, client_method, return_value=fake_actor_run) - caplog.set_level('WARNING') async with Actor: actor_method = getattr(Actor, actor_method_name) @@ -274,15 +272,28 @@ async def test_remote_method_warns_on_unsupported_webhook_fields( Webhook( event_types=['ACTOR.RUN.SUCCEEDED'], request_url='https://example.com', + payload_template='{"hello": "world"}', + headers_template='{"Authorization": "Bearer ..."}', idempotency_key='some-key', + ignore_ssl_errors=True, do_not_retry=True, ) ], ) - matching = [record for record in caplog.records if 'Ad-hoc webhooks do not support' in record.message] - assert len(matching) == 1 - assert '`idempotency_key`, `do_not_retry`' in matching[0].message + calls = apify_client_async_patcher.calls[client_resource][client_method] + assert len(calls) == 1 + _, kwargs = calls[0][0], calls[0][1] + (representation,) = kwargs['webhooks'] + assert representation.model_dump(by_alias=True, exclude_none=True) == { + 'eventTypes': ['ACTOR.RUN.SUCCEEDED'], + 'requestUrl': 'https://example.com', + 'payloadTemplate': '{"hello": "world"}', + 'headersTemplate': '{"Authorization": "Bearer ..."}', + 'idempotencyKey': 'some-key', + 'ignoreSslErrors': True, + 'doNotRetry': True, + } @pytest.mark.parametrize(('client_resource', 'client_method', 'actor_method_name', 'entity_id'), _ACTOR_REMOTE_METHODS) From f66a2e2557c72b8cd81048943b4a739926f9bee2 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 16 Jun 2026 16:37:04 +0200 Subject: [PATCH 3/7] new apify client --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c3add790..6db45fd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ keywords = [ "scraping", ] dependencies = [ - "apify-client>=3.0.0,<4.0.0", + "apify-client==3.0.3b2", "crawlee>=1.0.4,<2.0.0", "cachetools>=5.5.0", "cryptography>=42.0.0", diff --git a/uv.lock b/uv.lock index 8816cc39..f5045d3f 100644 --- a/uv.lock +++ b/uv.lock @@ -82,7 +82,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "apify-client", specifier = ">=3.0.0,<4.0.0" }, + { name = "apify-client", specifier = "==3.0.3b2" }, { name = "cachetools", specifier = ">=5.5.0" }, { name = "crawlee", specifier = ">=1.0.4,<2.0.0" }, { name = "cryptography", specifier = ">=42.0.0" }, @@ -123,7 +123,7 @@ dev = [ [[package]] name = "apify-client" -version = "3.0.2" +version = "3.0.3b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, @@ -131,9 +131,9 @@ dependencies = [ { name = "more-itertools" }, { name = "pydantic", extra = ["email"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/08/5d6c2fa64fd93b837325368aedaa23f57897030eff31a150873f3aa697e3/apify_client-3.0.2.tar.gz", hash = "sha256:e34dc9c8ac951154a0382adfce785d9658639c373425b43ac54df510d076ac0d", size = 122200, upload-time = "2026-05-26T07:23:43.691Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/55/ceb886d9169891fb48744af52e19306c608c6ea8c13b2e1008e824d39510/apify_client-3.0.3b2.tar.gz", hash = "sha256:3e85bd267a4a64c93ebe502ab167bc48aff6df5b7fcc2c35cf7a0ef8a558e537", size = 122835, upload-time = "2026-06-16T07:03:39.669Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/32/ac2ab06f8713f9a1313912815f57431d495cf04861a9f9a2436310dcf38e/apify_client-3.0.2-py3-none-any.whl", hash = "sha256:d78e188eb98e8357a21b57d9b31b139410b517eecc2d125c80f054cdc604296f", size = 141197, upload-time = "2026-05-26T07:23:42.039Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8e/66723e8bed56b803a357739009740e2b8c87f5375ab8386d59f1ad6f09f4/apify_client-3.0.3b2-py3-none-any.whl", hash = "sha256:d13a342cd007d1ea69775b3f9e357756796c8f23356968984e91fbf853cdc40b", size = 141610, upload-time = "2026-06-16T07:03:38.183Z" }, ] [[package]] From 02f59deb8332bb268394d19d51f740b608135327 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 16 Jun 2026 16:42:59 +0200 Subject: [PATCH 4/7] refactor: forward webhook fields by name instead of listing each one --- src/apify/_actor.py | 15 ++++++++------- src/apify/_webhook.py | 21 +++++++-------------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index e00e4663..d41b40e7 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -4,6 +4,7 @@ import sys import warnings from contextlib import suppress +from dataclasses import asdict from datetime import UTC, datetime, timedelta from functools import cached_property from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast, overload @@ -1283,15 +1284,15 @@ async def add_webhook(self, webhook: Webhook, *, idempotency_key: str | None = N if not self.configuration.actor_run_id: raise RuntimeError('actor_run_id cannot be None when running on the Apify platform.') + # `Webhook`'s field names match `webhooks().create()`'s parameters, so we forward them by name rather + # than listing each one. The deprecated `idempotency_key` argument, when given, overrides the instance value. + webhook_fields = asdict(webhook) + if idempotency_key is not None: + webhook_fields['idempotency_key'] = idempotency_key + await self.apify_client.webhooks().create( + **webhook_fields, actor_run_id=self.configuration.actor_run_id, - event_types=webhook.event_types, - request_url=webhook.request_url, - payload_template=webhook.payload_template, - headers_template=webhook.headers_template, - ignore_ssl_errors=webhook.ignore_ssl_errors, - do_not_retry=webhook.do_not_retry, - idempotency_key=idempotency_key if idempotency_key is not None else webhook.idempotency_key, is_ad_hoc=True, ) diff --git a/src/apify/_webhook.py b/src/apify/_webhook.py index 2bc2dc99..53cc3067 100644 --- a/src/apify/_webhook.py +++ b/src/apify/_webhook.py @@ -1,6 +1,6 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import asdict, dataclass from typing import TYPE_CHECKING from apify_client._models import WebhookRepresentation @@ -48,18 +48,11 @@ def __post_init__(self) -> None: def to_client_representations(webhooks: list[Webhook] | None) -> list[WebhookRepresentation] | None: - """Convert SDK webhooks to the ad-hoc representation accepted by the client's `start()` / `call()`.""" + """Convert SDK webhooks to the ad-hoc representation accepted by the client's `start()` / `call()`. + + `Webhook`'s field names are a subset of `WebhookRepresentation`'s, so we forward them by name rather than + listing each one. This way any field added to `Webhook` is propagated automatically. + """ if not webhooks: return None - return [ - WebhookRepresentation( - event_types=w.event_types, - request_url=w.request_url, - payload_template=w.payload_template, - headers_template=w.headers_template, - idempotency_key=w.idempotency_key, - ignore_ssl_errors=w.ignore_ssl_errors, - do_not_retry=w.do_not_retry, - ) - for w in webhooks - ] + return [WebhookRepresentation(**asdict(w)) for w in webhooks] From 0d133fb36eaefc73d2a0368fb7eccbc835a6c605 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 16 Jun 2026 16:47:58 +0200 Subject: [PATCH 5/7] Update --- src/apify/_actor.py | 3 +- src/apify/_webhook.py | 3 +- tests/unit/actor/test_actor_helpers.py | 44 -------------------------- 3 files changed, 4 insertions(+), 46 deletions(-) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index d41b40e7..20242a9a 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -1285,8 +1285,9 @@ async def add_webhook(self, webhook: Webhook, *, idempotency_key: str | None = N raise RuntimeError('actor_run_id cannot be None when running on the Apify platform.') # `Webhook`'s field names match `webhooks().create()`'s parameters, so we forward them by name rather - # than listing each one. The deprecated `idempotency_key` argument, when given, overrides the instance value. + # than listing each one. webhook_fields = asdict(webhook) + if idempotency_key is not None: webhook_fields['idempotency_key'] = idempotency_key diff --git a/src/apify/_webhook.py b/src/apify/_webhook.py index 53cc3067..55fa214c 100644 --- a/src/apify/_webhook.py +++ b/src/apify/_webhook.py @@ -55,4 +55,5 @@ def to_client_representations(webhooks: list[Webhook] | None) -> list[WebhookRep """ if not webhooks: return None - return [WebhookRepresentation(**asdict(w)) for w in webhooks] + + return [WebhookRepresentation(**asdict(webhook)) for webhook in webhooks] diff --git a/tests/unit/actor/test_actor_helpers.py b/tests/unit/actor/test_actor_helpers.py index 794dde44..5d943904 100644 --- a/tests/unit/actor/test_actor_helpers.py +++ b/tests/unit/actor/test_actor_helpers.py @@ -252,50 +252,6 @@ async def test_remote_method_with_webhooks( assert kwargs['webhooks'] is not None -@pytest.mark.parametrize(('client_resource', 'client_method', 'actor_method_name', 'entity_id'), _ACTOR_REMOTE_METHODS) -async def test_remote_method_forwards_all_webhook_fields( - apify_client_async_patcher: ApifyClientAsyncPatcher, - fake_actor_run: Run, - client_resource: str, - client_method: str, - actor_method_name: str, - entity_id: str, -) -> None: - """Test that start/call/call_task forward all `Webhook` fields to the client representation.""" - apify_client_async_patcher.patch(client_resource, client_method, return_value=fake_actor_run) - - async with Actor: - actor_method = getattr(Actor, actor_method_name) - await actor_method( - entity_id, - webhooks=[ - Webhook( - event_types=['ACTOR.RUN.SUCCEEDED'], - request_url='https://example.com', - payload_template='{"hello": "world"}', - headers_template='{"Authorization": "Bearer ..."}', - idempotency_key='some-key', - ignore_ssl_errors=True, - do_not_retry=True, - ) - ], - ) - - calls = apify_client_async_patcher.calls[client_resource][client_method] - assert len(calls) == 1 - _, kwargs = calls[0][0], calls[0][1] - (representation,) = kwargs['webhooks'] - assert representation.model_dump(by_alias=True, exclude_none=True) == { - 'eventTypes': ['ACTOR.RUN.SUCCEEDED'], - 'requestUrl': 'https://example.com', - 'payloadTemplate': '{"hello": "world"}', - 'headersTemplate': '{"Authorization": "Bearer ..."}', - 'idempotencyKey': 'some-key', - 'ignoreSslErrors': True, - 'doNotRetry': True, - } - - @pytest.mark.parametrize(('client_resource', 'client_method', 'actor_method_name', 'entity_id'), _ACTOR_REMOTE_METHODS) async def test_remote_method_with_timedelta_timeout( apify_client_async_patcher: ApifyClientAsyncPatcher, From fff249893b9d47cd850f93764c931a668860d2d5 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 16 Jun 2026 16:50:02 +0200 Subject: [PATCH 6/7] revert tmp client changes --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6db45fd3..c3add790 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ keywords = [ "scraping", ] dependencies = [ - "apify-client==3.0.3b2", + "apify-client>=3.0.0,<4.0.0", "crawlee>=1.0.4,<2.0.0", "cachetools>=5.5.0", "cryptography>=42.0.0", diff --git a/uv.lock b/uv.lock index f5045d3f..8816cc39 100644 --- a/uv.lock +++ b/uv.lock @@ -82,7 +82,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "apify-client", specifier = "==3.0.3b2" }, + { name = "apify-client", specifier = ">=3.0.0,<4.0.0" }, { name = "cachetools", specifier = ">=5.5.0" }, { name = "crawlee", specifier = ">=1.0.4,<2.0.0" }, { name = "cryptography", specifier = ">=42.0.0" }, @@ -123,7 +123,7 @@ dev = [ [[package]] name = "apify-client" -version = "3.0.3b2" +version = "3.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, @@ -131,9 +131,9 @@ dependencies = [ { name = "more-itertools" }, { name = "pydantic", extra = ["email"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/55/ceb886d9169891fb48744af52e19306c608c6ea8c13b2e1008e824d39510/apify_client-3.0.3b2.tar.gz", hash = "sha256:3e85bd267a4a64c93ebe502ab167bc48aff6df5b7fcc2c35cf7a0ef8a558e537", size = 122835, upload-time = "2026-06-16T07:03:39.669Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/08/5d6c2fa64fd93b837325368aedaa23f57897030eff31a150873f3aa697e3/apify_client-3.0.2.tar.gz", hash = "sha256:e34dc9c8ac951154a0382adfce785d9658639c373425b43ac54df510d076ac0d", size = 122200, upload-time = "2026-05-26T07:23:43.691Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/8e/66723e8bed56b803a357739009740e2b8c87f5375ab8386d59f1ad6f09f4/apify_client-3.0.3b2-py3-none-any.whl", hash = "sha256:d13a342cd007d1ea69775b3f9e357756796c8f23356968984e91fbf853cdc40b", size = 141610, upload-time = "2026-06-16T07:03:38.183Z" }, + { url = "https://files.pythonhosted.org/packages/d2/32/ac2ab06f8713f9a1313912815f57431d495cf04861a9f9a2436310dcf38e/apify_client-3.0.2-py3-none-any.whl", hash = "sha256:d78e188eb98e8357a21b57d9b31b139410b517eecc2d125c80f054cdc604296f", size = 141197, upload-time = "2026-05-26T07:23:42.039Z" }, ] [[package]] From d6b0008f54aba74be7e1796ce4aeefea2e1f4e71 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 16 Jun 2026 16:58:39 +0200 Subject: [PATCH 7/7] better --- src/apify/_webhook.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/apify/_webhook.py b/src/apify/_webhook.py index 55fa214c..ff084f6a 100644 --- a/src/apify/_webhook.py +++ b/src/apify/_webhook.py @@ -1,6 +1,6 @@ from __future__ import annotations -from dataclasses import asdict, dataclass +from dataclasses import dataclass from typing import TYPE_CHECKING from apify_client._models import WebhookRepresentation @@ -50,10 +50,11 @@ def __post_init__(self) -> None: def to_client_representations(webhooks: list[Webhook] | None) -> list[WebhookRepresentation] | None: """Convert SDK webhooks to the ad-hoc representation accepted by the client's `start()` / `call()`. - `Webhook`'s field names are a subset of `WebhookRepresentation`'s, so we forward them by name rather than - listing each one. This way any field added to `Webhook` is propagated automatically. + `Webhook`'s field names match `WebhookRepresentation`'s, so we let pydantic read them straight off the + instance rather than listing each one. This forwards any field the representation declares without changes + here, and never emits an undeclared field as a malformed snake_case extra. """ if not webhooks: return None - return [WebhookRepresentation(**asdict(webhook)) for webhook in webhooks] + return [WebhookRepresentation.model_validate(webhook, from_attributes=True) for webhook in webhooks]