From 94370d951e5fb20ef006770cee74357f3c68a157 Mon Sep 17 00:00:00 2001 From: Josef Prochazka Date: Fri, 2 May 2025 10:08:06 +0200 Subject: [PATCH 1/4] Add `validate_input` endpoint Update `run_input` for actor start Deprecate `content_type` for actor start --- src/apify_client/_utils.py | 8 ++- .../clients/resource_clients/actor.py | 71 ++++++++++++++++--- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/apify_client/_utils.py b/src/apify_client/_utils.py index 0bbc04e8..13282936 100644 --- a/src/apify_client/_utils.py +++ b/src/apify_client/_utils.py @@ -143,6 +143,10 @@ def encode_key_value_store_record_value(value: Any, content_type: str | None = N content_type = 'application/json; charset=utf-8' if 'application/json' in content_type and not is_file_or_bytes(value) and not isinstance(value, str): - value = json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False, default=str).encode('utf-8') + value = _to_json(value).encode('utf-8') - return (value, content_type) + return value, content_type + + +def _to_json(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False, default=str) diff --git a/src/apify_client/clients/resource_clients/actor.py b/src/apify_client/clients/resource_clients/actor.py index 76921769..8ab6dc49 100644 --- a/src/apify_client/clients/resource_clients/actor.py +++ b/src/apify_client/clients/resource_clients/actor.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging from typing import TYPE_CHECKING, Any from apify_shared.utils import ( @@ -9,7 +10,7 @@ parse_date_fields, ) -from apify_client._utils import encode_key_value_store_record_value, encode_webhook_list_to_base64, pluck_data +from apify_client._utils import _to_json, encode_webhook_list_to_base64, pluck_data from apify_client.clients.base import ResourceClient, ResourceClientAsync from apify_client.clients.resource_clients.actor_version import ActorVersionClient, ActorVersionClientAsync from apify_client.clients.resource_clients.actor_version_collection import ( @@ -30,6 +31,8 @@ from apify_shared.consts import ActorJobStatus, MetaOrigin +logger = logging.getLogger(__name__) + def get_actor_representation( *, @@ -216,7 +219,7 @@ def delete(self) -> None: def start( self, *, - run_input: Any = None, + run_input: str | dict | None = None, content_type: str | None = None, build: str | None = None, max_items: int | None = None, @@ -232,7 +235,7 @@ def start( Args: run_input: The input to pass to the Actor run. - content_type: The content type of the input. + content_type: Deprecated. build: Specifies the Actor build to run. It can be either a build tag or build number. By default, the run uses the build specified in the default run configuration for the Actor (typically latest). max_items: Maximum number of results that will be returned by this run. If the Actor is charged @@ -255,7 +258,11 @@ def start( Returns: The run object. """ - run_input, content_type = encode_key_value_store_record_value(run_input, content_type) + if content_type: + logger.warning('`content_type` is deprecated and not used anymore.') + + if not isinstance(run_input, str): + run_input = _to_json(run_input) request_params = self._params( build=build, @@ -270,7 +277,7 @@ def start( response = self.http_client.call( url=self._url('runs'), method='POST', - headers={'content-type': content_type}, + headers={'content-type': 'application/json'}, data=run_input, params=request_params, ) @@ -459,6 +466,27 @@ def webhooks(self) -> WebhookCollectionClient: """Retrieve a client for webhooks associated with this Actor.""" return WebhookCollectionClient(**self._sub_resource_init_options()) + def validate_input(self, run_input: str | bytes | dict | None = None) -> bool: + """Validate the input for the Actor. + + Args: + run_input: The input to validate. Either json string or a dictionary. + + Returns: + True if the input is valid, else raise an exception with validation error details. + """ + if not isinstance(run_input, str): + run_input = _to_json(run_input) + + self.http_client.call( + url=self._url('validate-input'), + method='POST', + headers={'content-type': 'application/json'}, + data=run_input, + ) + + return True + class ActorClientAsync(ResourceClientAsync): """Async sub-client for manipulating a single Actor.""" @@ -583,7 +611,7 @@ async def delete(self) -> None: async def start( self, *, - run_input: Any = None, + run_input: str | dict | None = None, content_type: str | None = None, build: str | None = None, max_items: int | None = None, @@ -599,7 +627,7 @@ async def start( Args: run_input: The input to pass to the Actor run. - content_type: The content type of the input. + content_type: Deprecated. build: Specifies the Actor build to run. It can be either a build tag or build number. By default, the run uses the build specified in the default run configuration for the Actor (typically latest). max_items: Maximum number of results that will be returned by this run. If the Actor is charged @@ -622,7 +650,11 @@ async def start( Returns: The run object. """ - run_input, content_type = encode_key_value_store_record_value(run_input, content_type) + if content_type: + logger.warning('`content_type` is deprecated and not used anymore.') + + if not isinstance(run_input, str): + run_input = _to_json(run_input) request_params = self._params( build=build, @@ -637,7 +669,7 @@ async def start( response = await self.http_client.call( url=self._url('runs'), method='POST', - headers={'content-type': content_type}, + headers={'content-type': 'application/json'}, data=run_input, params=request_params, ) @@ -829,3 +861,24 @@ def version(self, version_number: str) -> ActorVersionClientAsync: def webhooks(self) -> WebhookCollectionClientAsync: """Retrieve a client for webhooks associated with this Actor.""" return WebhookCollectionClientAsync(**self._sub_resource_init_options()) + + async def validate_input(self, run_input: str | dict | None = None) -> bool: + """Validate the input for the Actor. + + Args: + run_input: The input to validate. Either json string or a dictionary. + + Returns: + True if the input is valid, else raise an exception with validation error details. + """ + if not isinstance(run_input, str): + run_input = _to_json(run_input) + + await self.http_client.call( + url=self._url('validate-input'), + method='POST', + headers={'content-type': 'application/json'}, + data=run_input, + ) + + return True From 4aa335fc5344b645bd731e577b9279f69068ab61 Mon Sep 17 00:00:00 2001 From: Josef Prochazka Date: Fri, 2 May 2025 16:10:22 +0200 Subject: [PATCH 2/4] Keep previous input possibilities --- src/apify_client/_utils.py | 8 +-- .../clients/resource_clients/actor.py | 49 +++++++------------ 2 files changed, 21 insertions(+), 36 deletions(-) diff --git a/src/apify_client/_utils.py b/src/apify_client/_utils.py index 13282936..0bbc04e8 100644 --- a/src/apify_client/_utils.py +++ b/src/apify_client/_utils.py @@ -143,10 +143,6 @@ def encode_key_value_store_record_value(value: Any, content_type: str | None = N content_type = 'application/json; charset=utf-8' if 'application/json' in content_type and not is_file_or_bytes(value) and not isinstance(value, str): - value = _to_json(value).encode('utf-8') + value = json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False, default=str).encode('utf-8') - return value, content_type - - -def _to_json(value: Any) -> str: - return json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False, default=str) + return (value, content_type) diff --git a/src/apify_client/clients/resource_clients/actor.py b/src/apify_client/clients/resource_clients/actor.py index 8ab6dc49..d21e12fd 100644 --- a/src/apify_client/clients/resource_clients/actor.py +++ b/src/apify_client/clients/resource_clients/actor.py @@ -1,6 +1,5 @@ from __future__ import annotations -import logging from typing import TYPE_CHECKING, Any from apify_shared.utils import ( @@ -10,7 +9,7 @@ parse_date_fields, ) -from apify_client._utils import _to_json, encode_webhook_list_to_base64, pluck_data +from apify_client._utils import encode_key_value_store_record_value, encode_webhook_list_to_base64, pluck_data from apify_client.clients.base import ResourceClient, ResourceClientAsync from apify_client.clients.resource_clients.actor_version import ActorVersionClient, ActorVersionClientAsync from apify_client.clients.resource_clients.actor_version_collection import ( @@ -31,8 +30,6 @@ from apify_shared.consts import ActorJobStatus, MetaOrigin -logger = logging.getLogger(__name__) - def get_actor_representation( *, @@ -219,7 +216,7 @@ def delete(self) -> None: def start( self, *, - run_input: str | dict | None = None, + run_input: Any = None, content_type: str | None = None, build: str | None = None, max_items: int | None = None, @@ -235,7 +232,7 @@ def start( Args: run_input: The input to pass to the Actor run. - content_type: Deprecated. + content_type: The content type of the input. build: Specifies the Actor build to run. It can be either a build tag or build number. By default, the run uses the build specified in the default run configuration for the Actor (typically latest). max_items: Maximum number of results that will be returned by this run. If the Actor is charged @@ -258,11 +255,7 @@ def start( Returns: The run object. """ - if content_type: - logger.warning('`content_type` is deprecated and not used anymore.') - - if not isinstance(run_input, str): - run_input = _to_json(run_input) + run_input, content_type = encode_key_value_store_record_value(run_input, content_type) request_params = self._params( build=build, @@ -277,7 +270,7 @@ def start( response = self.http_client.call( url=self._url('runs'), method='POST', - headers={'content-type': 'application/json'}, + headers={'content-type': content_type}, data=run_input, params=request_params, ) @@ -466,22 +459,22 @@ def webhooks(self) -> WebhookCollectionClient: """Retrieve a client for webhooks associated with this Actor.""" return WebhookCollectionClient(**self._sub_resource_init_options()) - def validate_input(self, run_input: str | bytes | dict | None = None) -> bool: + def validate_input(self, run_input: Any = None, content_type: str | None = None) -> bool: """Validate the input for the Actor. Args: - run_input: The input to validate. Either json string or a dictionary. + run_input: The input to validate. + content_type: The content type of the input. Returns: True if the input is valid, else raise an exception with validation error details. """ - if not isinstance(run_input, str): - run_input = _to_json(run_input) + run_input, content_type = encode_key_value_store_record_value(run_input, content_type) self.http_client.call( url=self._url('validate-input'), method='POST', - headers={'content-type': 'application/json'}, + headers={'content-type': content_type}, data=run_input, ) @@ -611,7 +604,7 @@ async def delete(self) -> None: async def start( self, *, - run_input: str | dict | None = None, + run_input: Any = None, content_type: str | None = None, build: str | None = None, max_items: int | None = None, @@ -627,7 +620,7 @@ async def start( Args: run_input: The input to pass to the Actor run. - content_type: Deprecated. + content_type: The content type of the input. build: Specifies the Actor build to run. It can be either a build tag or build number. By default, the run uses the build specified in the default run configuration for the Actor (typically latest). max_items: Maximum number of results that will be returned by this run. If the Actor is charged @@ -650,11 +643,7 @@ async def start( Returns: The run object. """ - if content_type: - logger.warning('`content_type` is deprecated and not used anymore.') - - if not isinstance(run_input, str): - run_input = _to_json(run_input) + run_input, content_type = encode_key_value_store_record_value(run_input, content_type) request_params = self._params( build=build, @@ -669,7 +658,7 @@ async def start( response = await self.http_client.call( url=self._url('runs'), method='POST', - headers={'content-type': 'application/json'}, + headers={'content-type': content_type}, data=run_input, params=request_params, ) @@ -862,22 +851,22 @@ def webhooks(self) -> WebhookCollectionClientAsync: """Retrieve a client for webhooks associated with this Actor.""" return WebhookCollectionClientAsync(**self._sub_resource_init_options()) - async def validate_input(self, run_input: str | dict | None = None) -> bool: + async def validate_input(self, run_input: Any = None, content_type: str | None = None) -> bool: """Validate the input for the Actor. Args: - run_input: The input to validate. Either json string or a dictionary. + run_input: The input to validate. + content_type: The content type of the input. Returns: True if the input is valid, else raise an exception with validation error details. """ - if not isinstance(run_input, str): - run_input = _to_json(run_input) + run_input, content_type = encode_key_value_store_record_value(run_input, content_type) await self.http_client.call( url=self._url('validate-input'), method='POST', - headers={'content-type': 'application/json'}, + headers={'content-type': content_type}, data=run_input, ) From 4bbfca4dbe733eb3b298e71397d0009c91a08f51 Mon Sep 17 00:00:00 2001 From: Josef Prochazka Date: Tue, 6 May 2025 08:57:35 +0200 Subject: [PATCH 3/4] Update docstring --- src/apify_client/clients/resource_clients/actor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apify_client/clients/resource_clients/actor.py b/src/apify_client/clients/resource_clients/actor.py index d21e12fd..baeab86f 100644 --- a/src/apify_client/clients/resource_clients/actor.py +++ b/src/apify_client/clients/resource_clients/actor.py @@ -460,7 +460,7 @@ def webhooks(self) -> WebhookCollectionClient: return WebhookCollectionClient(**self._sub_resource_init_options()) def validate_input(self, run_input: Any = None, content_type: str | None = None) -> bool: - """Validate the input for the Actor. + """Validate an input for the Actor that defines an input schema. Args: run_input: The input to validate. @@ -852,7 +852,7 @@ def webhooks(self) -> WebhookCollectionClientAsync: return WebhookCollectionClientAsync(**self._sub_resource_init_options()) async def validate_input(self, run_input: Any = None, content_type: str | None = None) -> bool: - """Validate the input for the Actor. + """Validate an input for the Actor that defines an input schema. Args: run_input: The input to validate. From a452828a1d77e2a91ae249db8fb6388609c07b78 Mon Sep 17 00:00:00 2001 From: Josef Prochazka Date: Tue, 6 May 2025 09:07:52 +0200 Subject: [PATCH 4/4] Add build tag parameter --- src/apify_client/clients/resource_clients/actor.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/apify_client/clients/resource_clients/actor.py b/src/apify_client/clients/resource_clients/actor.py index baeab86f..e88d1078 100644 --- a/src/apify_client/clients/resource_clients/actor.py +++ b/src/apify_client/clients/resource_clients/actor.py @@ -459,11 +459,14 @@ def webhooks(self) -> WebhookCollectionClient: """Retrieve a client for webhooks associated with this Actor.""" return WebhookCollectionClient(**self._sub_resource_init_options()) - def validate_input(self, run_input: Any = None, content_type: str | None = None) -> bool: + def validate_input( + self, run_input: Any = None, *, build_tag: str | None = None, content_type: str | None = None + ) -> bool: """Validate an input for the Actor that defines an input schema. Args: run_input: The input to validate. + build_tag: The actor's build tag. content_type: The content type of the input. Returns: @@ -476,6 +479,7 @@ def validate_input(self, run_input: Any = None, content_type: str | None = None) method='POST', headers={'content-type': content_type}, data=run_input, + params=self._params(build=build_tag), ) return True @@ -851,11 +855,14 @@ def webhooks(self) -> WebhookCollectionClientAsync: """Retrieve a client for webhooks associated with this Actor.""" return WebhookCollectionClientAsync(**self._sub_resource_init_options()) - async def validate_input(self, run_input: Any = None, content_type: str | None = None) -> bool: + async def validate_input( + self, run_input: Any = None, *, build_tag: str | None = None, content_type: str | None = None + ) -> bool: """Validate an input for the Actor that defines an input schema. Args: run_input: The input to validate. + build_tag: The actor's build tag. content_type: The content type of the input. Returns: @@ -868,6 +875,7 @@ async def validate_input(self, run_input: Any = None, content_type: str | None = method='POST', headers={'content-type': content_type}, data=run_input, + params=self._params(build=build_tag), ) return True