Skip to content

Commit 9d791fa

Browse files
vdusekclaude
andcommitted
refactor: replace encode_webhook_list_to_base64 with WebhookRepresentationList model
Introduce `WebhookRepresentation` and `WebhookRepresentationList` Pydantic models in `_internal_models.py` to replace the standalone utility function. The encoding logic now lives as methods on the model (`to_base64`, `encode_to_base64`). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ac60efe commit 9d791fa

File tree

5 files changed

+71
-55
lines changed

5 files changed

+71
-55
lines changed

src/apify_client/_internal_models.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
from __future__ import annotations
44

5-
from pydantic import BaseModel, ConfigDict
5+
import json
6+
from base64 import b64encode
7+
from typing import Annotated, overload
68

7-
from apify_client._models import ActorJobStatus # noqa: TC001
9+
from pydantic import BaseModel, ConfigDict, Field, RootModel
10+
11+
from apify_client._models import ActorJobStatus, WebhookCreate
812

913

1014
class ActorJob(BaseModel):
@@ -27,3 +31,58 @@ class ActorJobResponse(BaseModel):
2731
model_config = ConfigDict(extra='allow')
2832

2933
data: ActorJob
34+
35+
36+
class WebhookRepresentation(BaseModel):
37+
"""Representation of a webhook for base64-encoded API transmission.
38+
39+
Contains only the fields needed for the webhook payload sent via query parameters.
40+
"""
41+
42+
model_config = ConfigDict(populate_by_name=True, extra='ignore')
43+
44+
event_types: Annotated[list[str], Field(alias='eventTypes')]
45+
request_url: Annotated[str, Field(alias='requestUrl')]
46+
payload_template: Annotated[str | None, Field(alias='payloadTemplate')] = None
47+
headers_template: Annotated[str | None, Field(alias='headersTemplate')] = None
48+
49+
50+
class WebhookRepresentationList(RootModel[list[WebhookRepresentation]]):
51+
"""List of webhook representations with base64 encoding support."""
52+
53+
@classmethod
54+
def from_webhooks(cls, webhooks: list[dict | WebhookCreate]) -> WebhookRepresentationList:
55+
"""Construct from a list of webhook dictionaries or `WebhookCreate` models."""
56+
representations = []
57+
for webhook in webhooks:
58+
webhook_dict = webhook.model_dump(exclude_none=True) if isinstance(webhook, WebhookCreate) else webhook
59+
representations.append(WebhookRepresentation.model_validate(webhook_dict))
60+
return cls(representations)
61+
62+
def to_base64(self) -> str:
63+
"""Encode this list of webhook representations to a base64 string."""
64+
data = [r.model_dump(by_alias=True, exclude_none=True) for r in self.root]
65+
return b64encode(json.dumps(data).encode('utf-8')).decode('ascii')
66+
67+
@overload
68+
@classmethod
69+
def encode_to_base64(cls, webhooks: None) -> None: ...
70+
71+
@overload
72+
@classmethod
73+
def encode_to_base64(cls, webhooks: list[dict | WebhookCreate]) -> str: ...
74+
75+
@classmethod
76+
def encode_to_base64(cls, webhooks: list[dict | WebhookCreate] | None) -> str | None:
77+
"""Encode a list of webhook dictionaries or `WebhookCreate` models to base64 for API transmission.
78+
79+
Args:
80+
webhooks: A list of webhook dictionaries or `WebhookCreate` models with keys like
81+
`event_types`, `request_url`, etc. If None, returns None.
82+
83+
Returns:
84+
A base64-encoded JSON string, or None if webhooks is None.
85+
"""
86+
if webhooks is None:
87+
return None
88+
return cls.from_webhooks(webhooks).to_base64()

src/apify_client/_resource_clients/actor.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pydantic import TypeAdapter
66

77
from apify_client._docs import docs_group
8+
from apify_client._internal_models import WebhookRepresentationList
89
from apify_client._models import (
910
Actor,
1011
ActorPermissionLevel,
@@ -28,7 +29,6 @@
2829
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
2930
from apify_client._utils import (
3031
encode_key_value_store_record_value,
31-
encode_webhook_list_to_base64,
3232
response_to_dict,
3333
to_seconds,
3434
)
@@ -271,7 +271,7 @@ def start(
271271
timeout=to_seconds(timeout, as_int=True),
272272
waitForFinish=wait_for_finish,
273273
forcePermissionLevel=force_permission_level.value if force_permission_level is not None else None,
274-
webhooks=encode_webhook_list_to_base64(webhooks),
274+
webhooks=WebhookRepresentationList.encode_to_base64(webhooks),
275275
)
276276

277277
response = self._http_client.call(
@@ -736,7 +736,7 @@ async def start(
736736
timeout=to_seconds(timeout, as_int=True),
737737
waitForFinish=wait_for_finish,
738738
forcePermissionLevel=force_permission_level.value if force_permission_level is not None else None,
739-
webhooks=encode_webhook_list_to_base64(webhooks),
739+
webhooks=WebhookRepresentationList.encode_to_base64(webhooks),
740740
)
741741

742742
response = await self._http_client.call(

src/apify_client/_resource_clients/task.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import TYPE_CHECKING, Any
44

55
from apify_client._docs import docs_group
6+
from apify_client._internal_models import WebhookRepresentationList
67
from apify_client._models import (
78
ActorStandby,
89
Run,
@@ -18,7 +19,6 @@
1819
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
1920
from apify_client._utils import (
2021
catch_not_found_or_throw,
21-
encode_webhook_list_to_base64,
2222
response_to_dict,
2323
to_seconds,
2424
)
@@ -198,7 +198,7 @@ def start(
198198
timeout=to_seconds(timeout, as_int=True),
199199
restartOnError=restart_on_error,
200200
waitForFinish=wait_for_finish,
201-
webhooks=encode_webhook_list_to_base64(webhooks),
201+
webhooks=WebhookRepresentationList.encode_to_base64(webhooks),
202202
)
203203

204204
response = self._http_client.call(
@@ -502,7 +502,7 @@ async def start(
502502
timeout=to_seconds(timeout, as_int=True),
503503
restartOnError=restart_on_error,
504504
waitForFinish=wait_for_finish,
505-
webhooks=encode_webhook_list_to_base64(webhooks),
505+
webhooks=WebhookRepresentationList.encode_to_base64(webhooks),
506506
)
507507

508508
response = await self._http_client.call(

src/apify_client/_utils.py

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,13 @@
77
import string
88
import time
99
import warnings
10-
from base64 import b64encode, urlsafe_b64encode
10+
from base64 import urlsafe_b64encode
1111
from http import HTTPStatus
1212
from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload
1313

1414
import impit
1515

1616
from apify_client._consts import OVERRIDABLE_DEFAULT_HEADERS
17-
from apify_client._models import WebhookCreate
1817
from apify_client.errors import InvalidResponseBodyError
1918

2019
if TYPE_CHECKING:
@@ -70,48 +69,6 @@ def catch_not_found_or_throw(exc: ApifyApiError) -> None:
7069
raise exc
7170

7271

73-
@overload
74-
def encode_webhook_list_to_base64(webhooks: None) -> None: ...
75-
76-
77-
@overload
78-
def encode_webhook_list_to_base64(webhooks: list[dict | WebhookCreate]) -> str: ...
79-
80-
81-
def encode_webhook_list_to_base64(webhooks: list[dict | WebhookCreate] | None) -> str | None:
82-
"""Encode a list of webhook dictionaries or `WebhookCreate` models to base64 for API transmission.
83-
84-
Args:
85-
webhooks: A list of webhook dictionaries or `WebhookCreate` models with keys like
86-
"event_types", "request_url", etc. If None, returns None.
87-
88-
Returns:
89-
A base64-encoded JSON string, or None if webhooks is None.
90-
"""
91-
if webhooks is None:
92-
return None
93-
94-
data = list[dict]()
95-
96-
for webhook in webhooks:
97-
webhook_dict = webhook.model_dump(exclude_none=True) if isinstance(webhook, WebhookCreate) else webhook
98-
99-
webhook_representation = {
100-
'eventTypes': list(webhook_dict['event_types']),
101-
'requestUrl': webhook_dict['request_url'],
102-
}
103-
104-
if 'payload_template' in webhook_dict:
105-
webhook_representation['payloadTemplate'] = webhook_dict['payload_template']
106-
107-
if 'headers_template' in webhook_dict:
108-
webhook_representation['headersTemplate'] = webhook_dict['headers_template']
109-
110-
data.append(webhook_representation)
111-
112-
return b64encode(json.dumps(data).encode('utf-8')).decode('ascii')
113-
114-
11572
def encode_key_value_store_record_value(value: Any, content_type: str | None = None) -> tuple[Any, str]:
11673
"""Encode a value for storage in a key-value store record.
11774

tests/unit/test_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import impit
77
import pytest
88

9+
from apify_client._internal_models import WebhookRepresentationList
910
from apify_client._models import WebhookEventType
1011
from apify_client._resource_clients._resource_client import ResourceClientBase
1112
from apify_client._utils import (
@@ -14,7 +15,6 @@
1415
create_storage_content_signature,
1516
encode_base62,
1617
encode_key_value_store_record_value,
17-
encode_webhook_list_to_base64,
1818
is_retryable_error,
1919
response_to_dict,
2020
response_to_list,
@@ -31,9 +31,9 @@ def test_to_safe_id() -> None:
3131

3232

3333
def test_encode_webhook_list_to_base64() -> None:
34-
assert encode_webhook_list_to_base64([]) == 'W10='
34+
assert WebhookRepresentationList.encode_to_base64([]) == 'W10='
3535
assert (
36-
encode_webhook_list_to_base64(
36+
WebhookRepresentationList.encode_to_base64(
3737
[
3838
{
3939
'event_types': [WebhookEventType.ACTOR_RUN_CREATED],

0 commit comments

Comments
 (0)