Skip to content

Commit 5c00f0e

Browse files
committed
Address feedback
1 parent 0d584bc commit 5c00f0e

File tree

4 files changed

+62
-27
lines changed

4 files changed

+62
-27
lines changed

src/apify_client/_resource_clients/actor.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -268,10 +268,6 @@ def start(
268268
"""
269269
run_input, content_type = encode_key_value_store_record_value(run_input, content_type)
270270

271-
validated_webhooks = (
272-
[WebhookCreate.model_validate(w) if isinstance(w, dict) else w for w in webhooks] if webhooks else []
273-
)
274-
275271
request_params = self._build_params(
276272
build=build,
277273
maxItems=max_items,
@@ -281,7 +277,7 @@ def start(
281277
timeout=to_seconds(run_timeout, as_int=True),
282278
waitForFinish=wait_for_finish,
283279
forcePermissionLevel=force_permission_level.value if force_permission_level is not None else None,
284-
webhooks=WebhookRepresentationList.from_webhooks(validated_webhooks).to_base64(),
280+
webhooks=WebhookRepresentationList.from_webhooks(webhooks or []).to_base64(),
285281
)
286282

287283
response = self._http_client.call(
@@ -768,10 +764,6 @@ async def start(
768764
"""
769765
run_input, content_type = encode_key_value_store_record_value(run_input, content_type)
770766

771-
validated_webhooks = (
772-
[WebhookCreate.model_validate(w) if isinstance(w, dict) else w for w in webhooks] if webhooks else []
773-
)
774-
775767
request_params = self._build_params(
776768
build=build,
777769
maxItems=max_items,
@@ -781,7 +773,7 @@ async def start(
781773
timeout=to_seconds(run_timeout, as_int=True),
782774
waitForFinish=wait_for_finish,
783775
forcePermissionLevel=force_permission_level.value if force_permission_level is not None else None,
784-
webhooks=WebhookRepresentationList.from_webhooks(validated_webhooks).to_base64(),
776+
webhooks=WebhookRepresentationList.from_webhooks(webhooks or []).to_base64(),
785777
)
786778

787779
response = await self._http_client.call(

src/apify_client/_resource_clients/task.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,18 +197,14 @@ def start(
197197
if isinstance(task_input, dict):
198198
task_input = TaskInput.model_validate(task_input)
199199

200-
validated_webhooks = (
201-
[WebhookCreate.model_validate(w) if isinstance(w, dict) else w for w in webhooks] if webhooks else []
202-
)
203-
204200
request_params = self._build_params(
205201
build=build,
206202
maxItems=max_items,
207203
memory=memory_mbytes,
208204
timeout=to_seconds(run_timeout, as_int=True),
209205
restartOnError=restart_on_error,
210206
waitForFinish=wait_for_finish,
211-
webhooks=WebhookRepresentationList.from_webhooks(validated_webhooks).to_base64(),
207+
webhooks=WebhookRepresentationList.from_webhooks(webhooks or []).to_base64(),
212208
)
213209

214210
response = self._http_client.call(
@@ -525,18 +521,14 @@ async def start(
525521
if isinstance(task_input, dict):
526522
task_input = TaskInput.model_validate(task_input)
527523

528-
validated_webhooks = (
529-
[WebhookCreate.model_validate(w) if isinstance(w, dict) else w for w in webhooks] if webhooks else []
530-
)
531-
532524
request_params = self._build_params(
533525
build=build,
534526
maxItems=max_items,
535527
memory=memory_mbytes,
536528
timeout=to_seconds(run_timeout, as_int=True),
537529
restartOnError=restart_on_error,
538530
waitForFinish=wait_for_finish,
539-
webhooks=WebhookRepresentationList.from_webhooks(validated_webhooks).to_base64(),
531+
webhooks=WebhookRepresentationList.from_webhooks(webhooks or []).to_base64(),
540532
)
541533

542534
response = await self._http_client.call(

src/apify_client/_types.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from datetime import timedelta
66
from typing import Annotated, Any, Literal
77

8-
from pydantic import AnyUrl, BaseModel, ConfigDict, Field, RootModel
8+
from pydantic import AnyUrl, BaseModel, ConfigDict, Field, RootModel, model_validator
99

1010
from apify_client._models import ActorJobStatus, WebhookCreate # noqa: TC001
1111

@@ -62,13 +62,20 @@ class WebhookRepresentationList(RootModel[list[WebhookRepresentation]]):
6262
"""List of webhook representations with base64 encoding support."""
6363

6464
@classmethod
65-
def from_webhooks(cls, webhooks: list[WebhookCreate]) -> WebhookRepresentationList:
66-
"""Construct from a list of `WebhookCreate` models."""
65+
def from_webhooks(cls, webhooks: list[dict | WebhookCreate]) -> WebhookRepresentationList:
66+
"""Construct from a list of `WebhookCreate` models or plain dicts.
67+
68+
Dicts are validated directly as `WebhookRepresentation`, so only the minimal ad-hoc webhook fields
69+
(`event_types`, `request_url`, and optionally `payload_template`/`headers_template`) are required.
70+
"""
6771
representations = list[WebhookRepresentation]()
6872

69-
for w in webhooks:
70-
webhook_dict = w.model_dump(mode='json', exclude_none=True)
71-
representations.append(WebhookRepresentation.model_validate(webhook_dict))
73+
for webhook in webhooks:
74+
if isinstance(webhook, dict):
75+
representations.append(WebhookRepresentation.model_validate(webhook))
76+
else:
77+
webhook_dict = webhook.model_dump(mode='json', exclude_none=True)
78+
representations.append(WebhookRepresentation.model_validate(webhook_dict))
7279

7380
return cls(representations)
7481

@@ -131,3 +138,9 @@ class RequestDeleteInput(BaseModel):
131138
Field(alias='uniqueKey', examples=['GET|60d83e70|e3b0c442|https://apify.com']),
132139
] = None
133140
"""A unique key used for request de-duplication."""
141+
142+
@model_validator(mode='after')
143+
def _check_at_least_one_identifier(self) -> RequestDeleteInput:
144+
if self.id is None and self.unique_key is None:
145+
raise ValueError('At least one of `id` or `unique_key` must be provided')
146+
return self

tests/unit/test_utils.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def test_to_safe_id() -> None:
3131
assert to_safe_id('user/resource/extra') == 'user~resource~extra'
3232

3333

34-
def test_encode_webhook_list_to_base64() -> None:
34+
def test_webhook_representation_list_to_base64() -> None:
3535
assert WebhookRepresentationList.from_webhooks([]).to_base64() is None
3636
assert (
3737
WebhookRepresentationList.from_webhooks(
@@ -53,6 +53,44 @@ def test_encode_webhook_list_to_base64() -> None:
5353
)
5454

5555

56+
def test_webhook_representation_list_from_dicts() -> None:
57+
"""Test that from_webhooks accepts plain dicts with the minimal ad-hoc webhook shape."""
58+
result = WebhookRepresentationList.from_webhooks(
59+
[
60+
{
61+
'event_types': ['ACTOR.RUN.CREATED'],
62+
'request_url': 'https://example.com/run-created',
63+
},
64+
{
65+
'event_types': ['ACTOR.RUN.SUCCEEDED'],
66+
'request_url': 'https://example.com/run-succeeded',
67+
'payload_template': '{"hello": "world"}',
68+
},
69+
]
70+
).to_base64()
71+
72+
assert result is not None
73+
74+
# Also verify round-trip: dicts and WebhookCreate models should produce the same base64
75+
result_from_models = WebhookRepresentationList.from_webhooks(
76+
[
77+
WebhookCreate(
78+
event_types=[WebhookEventType.ACTOR_RUN_CREATED],
79+
condition=WebhookCondition(),
80+
request_url=AnyUrl('https://example.com/run-created'),
81+
),
82+
WebhookCreate(
83+
event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED],
84+
condition=WebhookCondition(),
85+
request_url=AnyUrl('https://example.com/run-succeeded'),
86+
payload_template='{"hello": "world"}',
87+
),
88+
]
89+
).to_base64()
90+
91+
assert result == result_from_models
92+
93+
5694
@pytest.mark.parametrize(
5795
'exc',
5896
[

0 commit comments

Comments
 (0)