diff --git a/src/apify_client/_resource_clients/actor.py b/src/apify_client/_resource_clients/actor.py index 08f74005..c320f8a4 100644 --- a/src/apify_client/_resource_clients/actor.py +++ b/src/apify_client/_resource_clients/actor.py @@ -23,14 +23,11 @@ RunOrigin, RunResponse, UpdateActorRequest, + WebhookCreate, ) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._utils import ( - encode_key_value_store_record_value, - encode_webhook_list_to_base64, - response_to_dict, - to_seconds, -) +from apify_client._types import WebhookRepresentationList +from apify_client._utils import encode_key_value_store_record_value, response_to_dict, to_seconds if TYPE_CHECKING: from datetime import timedelta @@ -232,7 +229,7 @@ def start( run_timeout: timedelta | None = None, force_permission_level: ActorPermissionLevel | None = None, wait_for_finish: int | None = None, - webhooks: list[dict] | None = None, + webhooks: list[dict | WebhookCreate] | None = None, timeout: Timeout = 'long', ) -> Run: """Start the Actor and immediately return the Run object. @@ -271,6 +268,10 @@ def start( """ run_input, content_type = encode_key_value_store_record_value(run_input, content_type) + validated_webhooks = ( + [WebhookCreate.model_validate(w) if isinstance(w, dict) else w for w in webhooks] if webhooks else [] + ) + request_params = self._build_params( build=build, maxItems=max_items, @@ -280,7 +281,7 @@ def start( timeout=to_seconds(run_timeout, as_int=True), waitForFinish=wait_for_finish, forcePermissionLevel=force_permission_level.value if force_permission_level is not None else None, - webhooks=encode_webhook_list_to_base64(webhooks) if webhooks is not None else None, + webhooks=WebhookRepresentationList.from_webhooks(validated_webhooks).to_base64(), ) response = self._http_client.call( @@ -306,7 +307,7 @@ def call( restart_on_error: bool | None = None, memory_mbytes: int | None = None, run_timeout: timedelta | None = None, - webhooks: list[dict] | None = None, + webhooks: list[dict | WebhookCreate] | None = None, force_permission_level: ActorPermissionLevel | None = None, wait_duration: timedelta | None = None, logger: Logger | None | Literal['default'] = 'default', @@ -728,7 +729,7 @@ async def start( run_timeout: timedelta | None = None, force_permission_level: ActorPermissionLevel | None = None, wait_for_finish: int | None = None, - webhooks: list[dict] | None = None, + webhooks: list[dict | WebhookCreate] | None = None, timeout: Timeout = 'long', ) -> Run: """Start the Actor and immediately return the Run object. @@ -767,6 +768,10 @@ async def start( """ run_input, content_type = encode_key_value_store_record_value(run_input, content_type) + validated_webhooks = ( + [WebhookCreate.model_validate(w) if isinstance(w, dict) else w for w in webhooks] if webhooks else [] + ) + request_params = self._build_params( build=build, maxItems=max_items, @@ -776,7 +781,7 @@ async def start( timeout=to_seconds(run_timeout, as_int=True), waitForFinish=wait_for_finish, forcePermissionLevel=force_permission_level.value if force_permission_level is not None else None, - webhooks=encode_webhook_list_to_base64(webhooks) if webhooks is not None else None, + webhooks=WebhookRepresentationList.from_webhooks(validated_webhooks).to_base64(), ) response = await self._http_client.call( @@ -802,7 +807,7 @@ async def call( restart_on_error: bool | None = None, memory_mbytes: int | None = None, run_timeout: timedelta | None = None, - webhooks: list[dict] | None = None, + webhooks: list[dict | WebhookCreate] | None = None, force_permission_level: ActorPermissionLevel | None = None, wait_duration: timedelta | None = None, logger: Logger | None | Literal['default'] = 'default', diff --git a/src/apify_client/_resource_clients/request_queue.py b/src/apify_client/_resource_clients/request_queue.py index 4a3b2266..a33adea5 100644 --- a/src/apify_client/_resource_clients/request_queue.py +++ b/src/apify_client/_resource_clients/request_queue.py @@ -35,6 +35,7 @@ UnlockRequestsResult, ) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync +from apify_client._types import RequestDeleteInput, RequestInput from apify_client._utils import catch_not_found_or_throw, response_to_dict, to_seconds from apify_client.errors import ApifyApiError @@ -189,7 +190,7 @@ def list_and_lock_head( def add_request( self, - request: dict, + request: dict | RequestInput, *, forefront: bool | None = None, timeout: Timeout = 'short', @@ -206,12 +207,15 @@ def add_request( Returns: The added request. """ + if isinstance(request, dict): + request = RequestInput.model_validate(request) + request_params = self._build_params(forefront=forefront, clientKey=self.client_key) response = self._http_client.call( url=self._build_url('requests'), method='POST', - json=request, + json=request.model_dump(by_alias=True, exclude_none=True), params=request_params, timeout=timeout, ) @@ -248,7 +252,7 @@ def get_request(self, request_id: str, *, timeout: Timeout = 'short') -> Request def update_request( self, - request: dict, + request: dict | Request, *, forefront: bool | None = None, timeout: Timeout = 'medium', @@ -265,14 +269,15 @@ def update_request( Returns: The updated request. """ - request_id = request['id'] + if isinstance(request, dict): + request = Request.model_validate(request) request_params = self._build_params(forefront=forefront, clientKey=self.client_key) response = self._http_client.call( - url=self._build_url(f'requests/{request_id}'), + url=self._build_url(f'requests/{request.id}'), method='PUT', - json=request, + json=request.model_dump(by_alias=True, exclude_none=True), params=request_params, timeout=timeout, ) @@ -361,7 +366,7 @@ def delete_request_lock( def batch_add_requests( self, - requests: list[dict], + requests: list[dict | RequestInput], *, forefront: bool = False, max_parallel: int = 1, @@ -396,6 +401,11 @@ def batch_add_requests( if max_parallel != 1: raise NotImplementedError('max_parallel is only supported in async client') + requests_as_dicts = [ + (RequestInput.model_validate(r) if isinstance(r, dict) else r).model_dump(by_alias=True, exclude_none=True) + for r in requests + ] + request_params = self._build_params(clientKey=self.client_key, forefront=forefront) # Compute the payload size limit to ensure it doesn't exceed the maximum allowed size. @@ -403,7 +413,7 @@ def batch_add_requests( # Split the requests into batches, constrained by the max payload size and max requests per batch. batches = constrained_batches( - requests, + requests_as_dicts, max_size=payload_size_limit_bytes, max_count=_RQ_MAX_REQUESTS_PER_BATCH, ) @@ -444,7 +454,7 @@ def batch_add_requests( def batch_delete_requests( self, - requests: list[dict], + requests: list[dict | RequestDeleteInput], *, timeout: Timeout = 'short', ) -> BatchDeleteResult: @@ -456,13 +466,20 @@ def batch_delete_requests( requests: List of the requests to delete. timeout: Timeout for the API HTTP request. """ + requests_as_dicts = [ + (RequestDeleteInput.model_validate(r) if isinstance(r, dict) else r).model_dump( + by_alias=True, exclude_none=True + ) + for r in requests + ] + request_params = self._build_params(clientKey=self.client_key) response = self._http_client.call( url=self._build_url('requests/batch'), method='DELETE', params=request_params, - json=requests, + json=requests_as_dicts, timeout=timeout, ) @@ -658,7 +675,7 @@ async def list_and_lock_head( async def add_request( self, - request: dict, + request: dict | RequestInput, *, forefront: bool | None = None, timeout: Timeout = 'short', @@ -675,12 +692,15 @@ async def add_request( Returns: The added request. """ + if isinstance(request, dict): + request = RequestInput.model_validate(request) + request_params = self._build_params(forefront=forefront, clientKey=self.client_key) response = await self._http_client.call( url=self._build_url('requests'), method='POST', - json=request, + json=request.model_dump(by_alias=True, exclude_none=True), params=request_params, timeout=timeout, ) @@ -715,7 +735,7 @@ async def get_request(self, request_id: str, *, timeout: Timeout = 'short') -> R async def update_request( self, - request: dict, + request: dict | Request, *, forefront: bool | None = None, timeout: Timeout = 'medium', @@ -732,14 +752,15 @@ async def update_request( Returns: The updated request. """ - request_id = request['id'] + if isinstance(request, dict): + request = Request.model_validate(request) request_params = self._build_params(forefront=forefront, clientKey=self.client_key) response = await self._http_client.call( - url=self._build_url(f'requests/{request_id}'), + url=self._build_url(f'requests/{request.id}'), method='PUT', - json=request, + json=request.model_dump(by_alias=True, exclude_none=True), params=request_params, timeout=timeout, ) @@ -874,7 +895,7 @@ async def _batch_add_requests_worker( async def batch_add_requests( self, - requests: list[dict], + requests: list[dict | RequestInput], *, forefront: bool = False, max_parallel: int = 5, @@ -906,6 +927,11 @@ async def batch_add_requests( if min_delay_between_unprocessed_requests_retries: logger.warning('`min_delay_between_unprocessed_requests_retries` is deprecated and not used anymore.') + requests_as_dicts = [ + (RequestInput.model_validate(r) if isinstance(r, dict) else r).model_dump(by_alias=True, exclude_none=True) + for r in requests + ] + asyncio_queue: asyncio.Queue[Iterable[dict]] = asyncio.Queue() request_params = self._build_params(clientKey=self.client_key, forefront=forefront) @@ -914,7 +940,7 @@ async def batch_add_requests( # Split the requests into batches, constrained by the max payload size and max requests per batch. batches = constrained_batches( - requests, + requests_as_dicts, max_size=payload_size_limit_bytes, max_count=_RQ_MAX_REQUESTS_PER_BATCH, ) @@ -959,7 +985,7 @@ async def batch_add_requests( async def batch_delete_requests( self, - requests: list[dict], + requests: list[dict | RequestDeleteInput], *, timeout: Timeout = 'short', ) -> BatchDeleteResult: @@ -971,13 +997,20 @@ async def batch_delete_requests( requests: List of the requests to delete. timeout: Timeout for the API HTTP request. """ + requests_as_dicts = [ + (RequestDeleteInput.model_validate(r) if isinstance(r, dict) else r).model_dump( + by_alias=True, exclude_none=True + ) + for r in requests + ] + request_params = self._build_params(clientKey=self.client_key) response = await self._http_client.call( url=self._build_url('requests/batch'), method='DELETE', params=request_params, - json=requests, + json=requests_as_dicts, timeout=timeout, ) result = response_to_dict(response) diff --git a/src/apify_client/_resource_clients/run.py b/src/apify_client/_resource_clients/run.py index 2db19605..ac010578 100644 --- a/src/apify_client/_resource_clients/run.py +++ b/src/apify_client/_resource_clients/run.py @@ -13,12 +13,7 @@ from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync from apify_client._status_message_watcher import StatusMessageWatcher, StatusMessageWatcherAsync from apify_client._streamed_log import StreamedLog, StreamedLogAsync -from apify_client._utils import ( - encode_key_value_store_record_value, - response_to_dict, - to_safe_id, - to_seconds, -) +from apify_client._utils import encode_key_value_store_record_value, response_to_dict, to_safe_id, to_seconds if TYPE_CHECKING: import logging diff --git a/src/apify_client/_resource_clients/task.py b/src/apify_client/_resource_clients/task.py index 76a930ad..f67ccc40 100644 --- a/src/apify_client/_resource_clients/task.py +++ b/src/apify_client/_resource_clients/task.py @@ -13,14 +13,11 @@ TaskOptions, TaskResponse, UpdateTaskRequest, + WebhookCreate, ) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._utils import ( - catch_not_found_or_throw, - encode_webhook_list_to_base64, - response_to_dict, - to_seconds, -) +from apify_client._types import WebhookRepresentationList +from apify_client._utils import catch_not_found_or_throw, response_to_dict, to_seconds from apify_client.errors import ApifyApiError if TYPE_CHECKING: @@ -74,7 +71,7 @@ def update( self, *, name: str | None = None, - task_input: dict | None = None, + task_input: dict | TaskInput | None = None, build: str | None = None, max_items: int | None = None, memory_mbytes: int | None = None, @@ -119,10 +116,13 @@ def update( Returns: The updated task. """ + if isinstance(task_input, dict): + task_input = TaskInput.model_validate(task_input) + task_fields = UpdateTaskRequest( name=name, title=title, - input=TaskInput.model_validate(task_input) if task_input else None, + input=task_input, options=TaskOptions( build=build, max_items=max_items, @@ -154,14 +154,14 @@ def delete(self, *, timeout: Timeout = 'long') -> None: def start( self, *, - task_input: dict | None = None, + task_input: dict | TaskInput | None = None, build: str | None = None, max_items: int | None = None, memory_mbytes: int | None = None, run_timeout: timedelta | None = None, restart_on_error: bool | None = None, wait_for_finish: int | None = None, - webhooks: list[dict] | None = None, + webhooks: list[dict | WebhookCreate] | None = None, timeout: Timeout = 'long', ) -> Run: """Start the task and immediately return the Run object. @@ -194,6 +194,13 @@ def start( Returns: The run object. """ + if isinstance(task_input, dict): + task_input = TaskInput.model_validate(task_input) + + validated_webhooks = ( + [WebhookCreate.model_validate(w) if isinstance(w, dict) else w for w in webhooks] if webhooks else [] + ) + request_params = self._build_params( build=build, maxItems=max_items, @@ -201,14 +208,14 @@ def start( timeout=to_seconds(run_timeout, as_int=True), restartOnError=restart_on_error, waitForFinish=wait_for_finish, - webhooks=encode_webhook_list_to_base64(webhooks) if webhooks is not None else None, + webhooks=WebhookRepresentationList.from_webhooks(validated_webhooks).to_base64(), ) response = self._http_client.call( url=self._build_url('runs'), method='POST', headers={'content-type': 'application/json; charset=utf-8'}, - json=task_input, + json=task_input.model_dump() if task_input is not None else None, params=request_params, timeout=timeout, ) @@ -219,13 +226,13 @@ def start( def call( self, *, - task_input: dict | None = None, + task_input: dict | TaskInput | None = None, build: str | None = None, max_items: int | None = None, memory_mbytes: int | None = None, run_timeout: timedelta | None = None, restart_on_error: bool | None = None, - webhooks: list[dict] | None = None, + webhooks: list[dict | WebhookCreate] | None = None, wait_duration: timedelta | None = None, timeout: Timeout = 'long', ) -> Run | None: @@ -300,7 +307,7 @@ def get_input(self, *, timeout: Timeout = 'long') -> dict | None: catch_not_found_or_throw(exc) return None - def update_input(self, *, task_input: dict, timeout: Timeout = 'long') -> dict: + def update_input(self, *, task_input: dict | TaskInput, timeout: Timeout = 'long') -> dict: """Update the default input for this task. https://docs.apify.com/api/v2#/reference/actor-tasks/task-input-object/update-task-input @@ -312,11 +319,14 @@ def update_input(self, *, task_input: dict, timeout: Timeout = 'long') -> dict: Returns: The updated task input. """ + if isinstance(task_input, dict): + task_input = TaskInput.model_validate(task_input) + response = self._http_client.call( url=self._build_url('input'), method='PUT', params=self._build_params(), - json=task_input, + json=task_input.model_dump(), timeout=timeout, ) return response_to_dict(response) @@ -389,7 +399,7 @@ async def update( self, *, name: str | None = None, - task_input: dict | None = None, + task_input: dict | TaskInput | None = None, build: str | None = None, max_items: int | None = None, memory_mbytes: int | None = None, @@ -434,10 +444,13 @@ async def update( Returns: The updated task. """ + if isinstance(task_input, dict): + task_input = TaskInput.model_validate(task_input) + task_fields = UpdateTaskRequest( name=name, title=title, - input=TaskInput.model_validate(task_input) if task_input else None, + input=task_input, options=TaskOptions( build=build, max_items=max_items, @@ -469,14 +482,14 @@ async def delete(self, *, timeout: Timeout = 'long') -> None: async def start( self, *, - task_input: dict | None = None, + task_input: dict | TaskInput | None = None, build: str | None = None, max_items: int | None = None, memory_mbytes: int | None = None, run_timeout: timedelta | None = None, restart_on_error: bool | None = None, wait_for_finish: int | None = None, - webhooks: list[dict] | None = None, + webhooks: list[dict | WebhookCreate] | None = None, timeout: Timeout = 'long', ) -> Run: """Start the task and immediately return the Run object. @@ -509,6 +522,13 @@ async def start( Returns: The run object. """ + if isinstance(task_input, dict): + task_input = TaskInput.model_validate(task_input) + + validated_webhooks = ( + [WebhookCreate.model_validate(w) if isinstance(w, dict) else w for w in webhooks] if webhooks else [] + ) + request_params = self._build_params( build=build, maxItems=max_items, @@ -516,14 +536,14 @@ async def start( timeout=to_seconds(run_timeout, as_int=True), restartOnError=restart_on_error, waitForFinish=wait_for_finish, - webhooks=encode_webhook_list_to_base64(webhooks) if webhooks is not None else None, + webhooks=WebhookRepresentationList.from_webhooks(validated_webhooks).to_base64(), ) response = await self._http_client.call( url=self._build_url('runs'), method='POST', headers={'content-type': 'application/json; charset=utf-8'}, - json=task_input, + json=task_input.model_dump() if task_input is not None else None, params=request_params, timeout=timeout, ) @@ -534,13 +554,13 @@ async def start( async def call( self, *, - task_input: dict | None = None, + task_input: dict | TaskInput | None = None, build: str | None = None, max_items: int | None = None, memory_mbytes: int | None = None, run_timeout: timedelta | None = None, restart_on_error: bool | None = None, - webhooks: list[dict] | None = None, + webhooks: list[dict | WebhookCreate] | None = None, wait_duration: timedelta | None = None, timeout: Timeout = 'long', ) -> Run | None: @@ -614,7 +634,7 @@ async def get_input(self, *, timeout: Timeout = 'long') -> dict | None: catch_not_found_or_throw(exc) return None - async def update_input(self, *, task_input: dict, timeout: Timeout = 'long') -> dict: + async def update_input(self, *, task_input: dict | TaskInput, timeout: Timeout = 'long') -> dict: """Update the default input for this task. https://docs.apify.com/api/v2#/reference/actor-tasks/task-input-object/update-task-input @@ -626,11 +646,14 @@ async def update_input(self, *, task_input: dict, timeout: Timeout = 'long') -> Returns: The updated task input. """ + if isinstance(task_input, dict): + task_input = TaskInput.model_validate(task_input) + response = await self._http_client.call( url=self._build_url('input'), method='PUT', params=self._build_params(), - json=task_input, + json=task_input.model_dump(), timeout=timeout, ) return response_to_dict(response) diff --git a/src/apify_client/_resource_clients/task_collection.py b/src/apify_client/_resource_clients/task_collection.py index b6711d4a..2099b781 100644 --- a/src/apify_client/_resource_clients/task_collection.py +++ b/src/apify_client/_resource_clients/task_collection.py @@ -75,7 +75,7 @@ def create( memory_mbytes: int | None = None, max_items: int | None = None, restart_on_error: bool | None = None, - task_input: dict | None = None, + task_input: dict | TaskInput | None = None, title: str | None = None, actor_standby_desired_requests_per_actor_run: int | None = None, actor_standby_max_requests_per_actor_run: int | None = None, @@ -116,11 +116,14 @@ def create( Returns: The created task. """ + if isinstance(task_input, dict): + task_input = TaskInput.model_validate(task_input) + task_fields = CreateTaskRequest( act_id=actor_id, name=name, title=title, - input=TaskInput.model_validate(task_input) if task_input else None, + input=task_input, options=TaskOptions( build=build, max_items=max_items, @@ -193,7 +196,7 @@ async def create( memory_mbytes: int | None = None, max_items: int | None = None, restart_on_error: bool | None = None, - task_input: dict | None = None, + task_input: dict | TaskInput | None = None, title: str | None = None, actor_standby_desired_requests_per_actor_run: int | None = None, actor_standby_max_requests_per_actor_run: int | None = None, @@ -234,11 +237,14 @@ async def create( Returns: The created task. """ + if isinstance(task_input, dict): + task_input = TaskInput.model_validate(task_input) + task_fields = CreateTaskRequest( act_id=actor_id, name=name, title=title, - input=TaskInput.model_validate(task_input) if task_input else None, + input=task_input, options=TaskOptions( build=build, max_items=max_items, diff --git a/src/apify_client/_types.py b/src/apify_client/_types.py index 3d49395a..994583e8 100644 --- a/src/apify_client/_types.py +++ b/src/apify_client/_types.py @@ -1,11 +1,13 @@ from __future__ import annotations +import json +from base64 import b64encode from datetime import timedelta -from typing import Any, Literal +from typing import Annotated, Any, Literal -from pydantic import BaseModel, ConfigDict +from pydantic import AnyUrl, BaseModel, ConfigDict, Field, RootModel -from apify_client._models import ActorJobStatus # noqa: TC001 +from apify_client._models import ActorJobStatus, WebhookCreate # noqa: TC001 Timeout = timedelta | Literal['no_timeout', 'short', 'medium', 'long'] """Type for the `timeout` parameter on resource client methods. @@ -26,7 +28,7 @@ class ActorJob(BaseModel): Used for validation during polling operations. Allows extra fields so the full response data is preserved. """ - model_config = ConfigDict(extra='allow') + model_config = ConfigDict(extra='allow', populate_by_name=True) status: ActorJobStatus @@ -37,6 +39,95 @@ class ActorJobResponse(BaseModel): Used for minimal validation during polling operations. Allows extra fields so the full response data is preserved. """ - model_config = ConfigDict(extra='allow') + model_config = ConfigDict(extra='allow', populate_by_name=True) data: ActorJob + + +class WebhookRepresentation(BaseModel): + """Representation of a webhook for base64-encoded API transmission. + + Contains only the fields needed for the webhook payload sent via query parameters. + """ + + model_config = ConfigDict(populate_by_name=True, extra='ignore') + + event_types: Annotated[list[str], Field(alias='eventTypes')] + request_url: Annotated[str, Field(alias='requestUrl')] + payload_template: Annotated[str | None, Field(alias='payloadTemplate')] = None + headers_template: Annotated[str | None, Field(alias='headersTemplate')] = None + + +class WebhookRepresentationList(RootModel[list[WebhookRepresentation]]): + """List of webhook representations with base64 encoding support.""" + + @classmethod + def from_webhooks(cls, webhooks: list[WebhookCreate]) -> WebhookRepresentationList: + """Construct from a list of `WebhookCreate` models.""" + representations = list[WebhookRepresentation]() + + for w in webhooks: + webhook_dict = w.model_dump(mode='json', exclude_none=True) + representations.append(WebhookRepresentation.model_validate(webhook_dict)) + + return cls(representations) + + def to_base64(self) -> str | None: + """Encode this list of webhook representations to a base64 string. + + Returns `None` if the list is empty, so that the query parameter is omitted. + """ + if not self.root: + return None + + data = [r.model_dump(by_alias=True, exclude_none=True) for r in self.root] + json_string = json.dumps(data).encode(encoding='utf-8') + return b64encode(json_string).decode(encoding='ascii') + + +class RequestInput(BaseModel): + """Input model for adding requests to a request queue. + + Both `url` and `unique_key` are required. The API defaults `method` to `GET` when not provided. + """ + + model_config = ConfigDict( + extra='allow', + populate_by_name=True, + ) + + id: Annotated[str | None, Field(examples=['sbJ7klsdf7ujN9l'])] = None + """A unique identifier assigned to the request.""" + + unique_key: Annotated[ + str, + Field(alias='uniqueKey', examples=['GET|60d83e70|e3b0c442|https://apify.com']), + ] + """A unique key used for request de-duplication.""" + + url: Annotated[AnyUrl, Field(examples=['https://apify.com'])] + """The URL of the request.""" + + method: Annotated[str | None, Field(examples=['GET'])] = None + """The HTTP method of the request. Defaults to `GET` on the API side if not provided.""" + + +class RequestDeleteInput(BaseModel): + """Input model for deleting requests from a request queue. + + Requests are identified by `id` or `unique_key`. At least one must be provided. + """ + + model_config = ConfigDict( + extra='allow', + populate_by_name=True, + ) + + id: Annotated[str | None, Field(examples=['sbJ7klsdf7ujN9l'])] = None + """A unique identifier assigned to the request.""" + + unique_key: Annotated[ + str | None, + Field(alias='uniqueKey', examples=['GET|60d83e70|e3b0c442|https://apify.com']), + ] = None + """A unique key used for request de-duplication.""" diff --git a/src/apify_client/_utils.py b/src/apify_client/_utils.py index 3ce1a1b7..5f03836c 100644 --- a/src/apify_client/_utils.py +++ b/src/apify_client/_utils.py @@ -7,7 +7,7 @@ import string import time import warnings -from base64 import b64encode, urlsafe_b64encode +from base64 import urlsafe_b64encode from http import HTTPStatus from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload @@ -69,31 +69,6 @@ def catch_not_found_or_throw(exc: ApifyApiError) -> None: raise exc -def encode_webhook_list_to_base64(webhooks: list[dict]) -> str: - """Encode a list of webhook dictionaries to base64 for API transmission. - - Args: - webhooks: A list of webhook dictionaries with keys like "event_types", "request_url", etc. - - Returns: - A base64-encoded JSON string. - """ - data = list[dict]() - - for webhook in webhooks: - webhook_representation = { - 'eventTypes': list(webhook['event_types']), - 'requestUrl': webhook['request_url'], - } - if 'payload_template' in webhook: - webhook_representation['payloadTemplate'] = webhook['payload_template'] - if 'headers_template' in webhook: - webhook_representation['headersTemplate'] = webhook['headers_template'] - data.append(webhook_representation) - - return b64encode(json.dumps(data).encode('utf-8')).decode('ascii') - - def encode_key_value_store_record_value(value: Any, content_type: str | None = None) -> tuple[Any, str]: """Encode a value for storage in a key-value store record. diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index bdaef624..7528b7e7 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -5,16 +5,17 @@ import impit import pytest +from pydantic import AnyUrl -from apify_client._models import WebhookEventType +from apify_client._models import WebhookCondition, WebhookCreate, WebhookEventType from apify_client._resource_clients._resource_client import ResourceClientBase +from apify_client._types import WebhookRepresentationList from apify_client._utils import ( catch_not_found_or_throw, create_hmac_signature, create_storage_content_signature, encode_base62, encode_key_value_store_record_value, - encode_webhook_list_to_base64, is_retryable_error, response_to_dict, response_to_list, @@ -31,21 +32,23 @@ def test_to_safe_id() -> None: def test_encode_webhook_list_to_base64() -> None: - assert encode_webhook_list_to_base64([]) == 'W10=' + assert WebhookRepresentationList.from_webhooks([]).to_base64() is None assert ( - encode_webhook_list_to_base64( + WebhookRepresentationList.from_webhooks( [ - { - 'event_types': [WebhookEventType.ACTOR_RUN_CREATED], - 'request_url': 'https://example.com/run-created', - }, - { - 'event_types': [WebhookEventType.ACTOR_RUN_SUCCEEDED], - 'request_url': 'https://example.com/run-succeeded', - 'payload_template': '{"hello": "world", "resource":{{resource}}}', - }, + WebhookCreate( + event_types=[WebhookEventType.ACTOR_RUN_CREATED], + condition=WebhookCondition(), + request_url=AnyUrl('https://example.com/run-created'), + ), + WebhookCreate( + event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED], + condition=WebhookCondition(), + request_url=AnyUrl('https://example.com/run-succeeded'), + payload_template='{"hello": "world", "resource":{{resource}}}', + ), ] - ) + ).to_base64() == 'W3siZXZlbnRUeXBlcyI6IFsiQUNUT1IuUlVOLkNSRUFURUQiXSwgInJlcXVlc3RVcmwiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbS9ydW4tY3JlYXRlZCJ9LCB7ImV2ZW50VHlwZXMiOiBbIkFDVE9SLlJVTi5TVUNDRUVERUQiXSwgInJlcXVlc3RVcmwiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbS9ydW4tc3VjY2VlZGVkIiwgInBheWxvYWRUZW1wbGF0ZSI6ICJ7XCJoZWxsb1wiOiBcIndvcmxkXCIsIFwicmVzb3VyY2VcIjp7e3Jlc291cmNlfX19In1d' # noqa: E501 )