From 4829e3a303a5b2d3ac462092242293bd92801a52 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 24 Feb 2026 21:02:35 +0100 Subject: [PATCH 1/2] refactor: Use Pydantic request models and simplify base client internals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace inline dicts in resource clients with Pydantic request models for type-safe request construction - Move payload cleanup (None filtering) into base class as _clean_json_payload, replacing filter_none_values utility - Simplify _create/_update signatures to accept **kwargs - Add proper type conversions for list[dict] → list[Model], str → AnyUrl, dict → TaskInput - Remove _representations.py (orphaned after Pydantic model migration) - Remove enum_to_value utility (redundant with StrEnum) Co-Authored-By: Claude Opus 4.6 --- src/apify_client/_consts.py | 1 - src/apify_client/_models.py | 90 ++++-- src/apify_client/_representations.py | 302 ------------------ .../_resource_clients/_resource_client.py | 57 +++- src/apify_client/_resource_clients/actor.py | 128 ++++---- .../_resource_clients/actor_collection.py | 98 +++--- .../_resource_clients/actor_env_var.py | 31 +- .../actor_env_var_collection.py | 18 +- .../_resource_clients/actor_version.py | 44 +-- .../actor_version_collection.py | 36 ++- src/apify_client/_resource_clients/dataset.py | 17 +- .../_resource_clients/dataset_collection.py | 5 +- .../_resource_clients/key_value_store.py | 17 +- .../key_value_store_collection.py | 5 +- .../_resource_clients/request_queue.py | 18 +- src/apify_client/_resource_clients/run.py | 27 +- .../_resource_clients/run_collection.py | 5 +- .../_resource_clients/schedule.py | 32 +- .../_resource_clients/schedule_collection.py | 29 +- src/apify_client/_resource_clients/task.py | 83 ++--- .../_resource_clients/task_collection.py | 80 +++-- src/apify_client/_resource_clients/user.py | 6 +- src/apify_client/_resource_clients/webhook.py | 49 +-- .../_resource_clients/webhook_collection.py | 54 ++-- src/apify_client/_utils.py | 82 +---- tests/unit/test_utils.py | 47 +-- 26 files changed, 510 insertions(+), 851 deletions(-) delete mode 100644 src/apify_client/_representations.py diff --git a/src/apify_client/_consts.py b/src/apify_client/_consts.py index e61491f1..892d1b46 100644 --- a/src/apify_client/_consts.py +++ b/src/apify_client/_consts.py @@ -25,7 +25,6 @@ DEFAULT_MIN_DELAY_BETWEEN_RETRIES = timedelta(milliseconds=500) """Default minimum delay between retries.""" - DEFAULT_WAIT_FOR_FINISH = timedelta(seconds=999999) """Default maximum wait time for job completion (effectively infinite).""" diff --git a/src/apify_client/_models.py b/src/apify_client/_models.py index 26231768..eac92b15 100644 --- a/src/apify_client/_models.py +++ b/src/apify_client/_models.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: -# filename: https://docs.apify.com/api/openapi.json -# timestamp: 2026-02-24T08:34:43+00:00 +# filename: openapi.json +# timestamp: 2026-02-24T14:39:16+00:00 from __future__ import annotations @@ -258,14 +258,45 @@ class FreeActorPricingInfo(CommonActorPricingInfo): pricing_model: Annotated[Literal['FREE'], Field(alias='pricingModel')] +class ActorPermissionLevel(StrEnum): + """Determines permissions that the Actor requires to run. For more information, see the [Actor permissions documentation](https://docs.apify.com/platform/actors/development/permissions).""" + + LIMITED_PERMISSIONS = 'LIMITED_PERMISSIONS' + FULL_PERMISSIONS = 'FULL_PERMISSIONS' + + class DefaultRunOptions(BaseModel): model_config = ConfigDict( extra='allow', ) - build: Annotated[str, Field(examples=['latest'])] + build: Annotated[str | None, Field(examples=['latest'])] = None timeout_secs: Annotated[int | None, Field(alias='timeoutSecs', examples=[3600])] = None - memory_mbytes: Annotated[int, Field(alias='memoryMbytes', examples=[2048])] + memory_mbytes: Annotated[int | None, Field(alias='memoryMbytes', examples=[2048])] = None restart_on_error: Annotated[bool | None, Field(alias='restartOnError', examples=[False])] = None + max_items: Annotated[int | None, Field(alias='maxItems')] = None + force_permission_level: Annotated[ActorPermissionLevel | None, Field(alias='forcePermissionLevel')] = None + + +class ActorStandby(BaseModel): + model_config = ConfigDict( + extra='allow', + ) + is_enabled: Annotated[bool | None, Field(alias='isEnabled')] = None + desired_requests_per_actor_run: Annotated[int | None, Field(alias='desiredRequestsPerActorRun')] = None + max_requests_per_actor_run: Annotated[int | None, Field(alias='maxRequestsPerActorRun')] = None + idle_timeout_secs: Annotated[int | None, Field(alias='idleTimeoutSecs')] = None + build: str | None = None + memory_mbytes: Annotated[int | None, Field(alias='memoryMbytes')] = None + disable_standby_fields_override: Annotated[bool | None, Field(alias='disableStandbyFieldsOverride')] = None + should_pass_actor_input: Annotated[bool | None, Field(alias='shouldPassActorInput')] = None + + +class ExampleRunInput(BaseModel): + model_config = ConfigDict( + extra='allow', + ) + body: Annotated[str | None, Field(examples=['{ "helloWorld": 123 }'])] = None + content_type: Annotated[str | None, Field(alias='contentType', examples=['application/json; charset=utf-8'])] = None class CreateActorRequest(BaseModel): @@ -292,21 +323,9 @@ class CreateActorRequest(BaseModel): ] = None categories: list[str] | None = None default_run_options: Annotated[DefaultRunOptions | None, Field(alias='defaultRunOptions')] = None - - -class ActorPermissionLevel(StrEnum): - """Determines permissions that the Actor requires to run. For more information, see the [Actor permissions documentation](https://docs.apify.com/platform/actors/development/permissions).""" - - LIMITED_PERMISSIONS = 'LIMITED_PERMISSIONS' - FULL_PERMISSIONS = 'FULL_PERMISSIONS' - - -class ExampleRunInput(BaseModel): - model_config = ConfigDict( - extra='allow', - ) - body: Annotated[str, Field(examples=['{ "helloWorld": 123 }'])] - content_type: Annotated[str, Field(alias='contentType', examples=['application/json; charset=utf-8'])] + actor_standby: Annotated[ActorStandby | None, Field(alias='actorStandby')] = None + example_run_input: Annotated[ExampleRunInput | None, Field(alias='exampleRunInput')] = None + is_deprecated: Annotated[bool | None, Field(alias='isDeprecated')] = None class TaggedBuildInfo(BaseModel): @@ -367,6 +386,7 @@ class Actor(BaseModel): """ A brief, LLM-generated readme summary """ + actor_standby: Annotated[ActorStandby | None, Field(alias='actorStandby')] = None class ActorResponse(BaseModel): @@ -415,15 +435,15 @@ class UpdateActorRequest(BaseModel): model_config = ConfigDict( extra='allow', ) - name: Annotated[str, Field(examples=['MyActor'])] + name: Annotated[str | None, Field(examples=['MyActor'])] = None description: Annotated[str | None, Field(examples=['My favourite actor!'])] = None - is_public: Annotated[bool, Field(alias='isPublic', examples=[False])] + is_public: Annotated[bool | None, Field(alias='isPublic', examples=[False])] = None actor_permission_level: Annotated[ActorPermissionLevel | None, Field(alias='actorPermissionLevel')] = None seo_title: Annotated[str | None, Field(alias='seoTitle', examples=['My actor'])] = None seo_description: Annotated[str | None, Field(alias='seoDescription', examples=['My actor is the best'])] = None title: Annotated[str | None, Field(examples=['My Actor'])] = None restart_on_error: Annotated[bool | None, Field(alias='restartOnError', examples=[False])] = None - versions: list[CreateOrUpdateVersionRequest] + versions: list[CreateOrUpdateVersionRequest] | None = None pricing_infos: Annotated[ list[ PayPerEventActorPricingInfo @@ -481,6 +501,9 @@ class UpdateActorRequest(BaseModel): ``` """ + actor_standby: Annotated[ActorStandby | None, Field(alias='actorStandby')] = None + example_run_input: Annotated[ExampleRunInput | None, Field(alias='exampleRunInput')] = None + is_deprecated: Annotated[bool | None, Field(alias='isDeprecated')] = None class ListOfVersions(BaseModel): @@ -566,9 +589,7 @@ class ExampleWebhookDispatch(BaseModel): extra='allow', ) status: WebhookDispatchStatus - finished_at: Annotated[AwareDatetime | None, Field(alias='finishedAt', examples=['2019-12-13T08:36:13.202Z'])] = ( - None - ) + finished_at: Annotated[AwareDatetime, Field(alias='finishedAt', examples=['2019-12-13T08:36:13.202Z'])] class WebhookStats(BaseModel): @@ -1178,6 +1199,7 @@ class TaskOptions(BaseModel): timeout_secs: Annotated[int | None, Field(alias='timeoutSecs', examples=[300])] = None memory_mbytes: Annotated[int | None, Field(alias='memoryMbytes', examples=[128])] = None restart_on_error: Annotated[bool | None, Field(alias='restartOnError', examples=[False])] = None + max_items: Annotated[int | None, Field(alias='maxItems')] = None class TaskInput(BaseModel): @@ -1199,6 +1221,8 @@ class CreateTaskRequest(BaseModel): name: Annotated[str, Field(examples=['my-task'])] options: TaskOptions | None = None input: TaskInput | None = None + title: str | None = None + actor_standby: Annotated[ActorStandby | None, Field(alias='actorStandby')] = None class Task(BaseModel): @@ -1217,6 +1241,8 @@ class Task(BaseModel): options: TaskOptions | None = None input: TaskInput | None = None standby_url: Annotated[AnyUrl | None, Field(alias='standbyUrl')] = None + title: str | None = None + actor_standby: Annotated[ActorStandby | None, Field(alias='actorStandby')] = None class TaskResponse(BaseModel): @@ -1232,17 +1258,11 @@ class UpdateTaskRequest(BaseModel): model_config = ConfigDict( extra='allow', ) - id: Annotated[str, Field(examples=['ZxLNxrRaZrSjuhT9y'])] - user_id: Annotated[str, Field(alias='userId', examples=['BPWZBd7Z9c746JAnF'])] - act_id: Annotated[str, Field(alias='actId', examples=['asADASadYvn4mBZmm'])] - name: Annotated[str, Field(examples=['my-task'])] - username: Annotated[str | None, Field(examples=['janedoe'])] = None - created_at: Annotated[AwareDatetime, Field(alias='createdAt', examples=['2018-10-26T07:23:14.855Z'])] - modified_at: Annotated[AwareDatetime, Field(alias='modifiedAt', examples=['2018-10-26T13:30:49.578Z'])] - removed_at: Annotated[AwareDatetime | None, Field(alias='removedAt')] = None - stats: TaskStats | None = None + name: Annotated[str | None, Field(examples=['my-task'])] = None + title: str | None = None options: TaskOptions | None = None input: TaskInput | None = None + actor_standby: Annotated[ActorStandby | None, Field(alias='actorStandby')] = None class Webhook(BaseModel): @@ -2504,6 +2524,7 @@ class ScheduleCreate(BaseModel): cron_expression: Annotated[str | None, Field(alias='cronExpression', examples=['* * * * *'])] = None timezone: Annotated[str | None, Field(examples=['UTC'])] = None description: Annotated[str | None, Field(examples=['Schedule of actor ...'])] = None + title: str | None = None actions: list[ScheduleCreateActions] | None = None @@ -2534,6 +2555,7 @@ class Schedule(BaseModel): modified_at: Annotated[AwareDatetime, Field(alias='modifiedAt', examples=['2019-12-20T06:33:11.202Z'])] next_run_at: Annotated[AwareDatetime | None, Field(alias='nextRunAt', examples=['2019-04-12T07:34:10.202Z'])] = None last_run_at: Annotated[AwareDatetime | None, Field(alias='lastRunAt', examples=['2019-04-12T07:33:10.202Z'])] = None + title: str | None = None actions: list[ScheduleActions] diff --git a/src/apify_client/_representations.py b/src/apify_client/_representations.py deleted file mode 100644 index a0a24423..00000000 --- a/src/apify_client/_representations.py +++ /dev/null @@ -1,302 +0,0 @@ -"""Helper functions for building API request representations. - -This module provides utilities for constructing dictionary representations of API resources. These representations -are used when creating or updating resources via the Apify API. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -from apify_client._utils import enum_to_value, to_seconds - -if TYPE_CHECKING: - from datetime import timedelta - - from apify_client._models import ActorPermissionLevel, VersionSourceType, WebhookEventType - - -def build_actor_standby_dict( - *, - is_enabled: bool | None = None, - desired_requests_per_actor_run: int | None = None, - max_requests_per_actor_run: int | None = None, - idle_timeout: timedelta | None = None, - build: str | None = None, - memory_mbytes: int | None = None, -) -> dict: - """Build Actor standby configuration dictionary.""" - return { - 'isEnabled': is_enabled, - 'desiredRequestsPerActorRun': desired_requests_per_actor_run, - 'maxRequestsPerActorRun': max_requests_per_actor_run, - 'idleTimeoutSecs': to_seconds(idle_timeout, as_int=True), - 'build': build, - 'memoryMbytes': memory_mbytes, - } - - -def build_default_run_options_dict( - *, - build: str | None = None, - max_items: int | None = None, - memory_mbytes: int | None = None, - timeout: timedelta | None = None, - restart_on_error: bool | None = None, - force_permission_level: ActorPermissionLevel | None = None, -) -> dict: - """Build default run options dictionary for Actor.""" - return { - 'build': build, - 'maxItems': max_items, - 'memoryMbytes': memory_mbytes, - 'timeoutSecs': to_seconds(timeout, as_int=True), - 'restartOnError': restart_on_error, - 'forcePermissionLevel': force_permission_level, - } - - -def build_task_options_dict( - *, - build: str | None = None, - max_items: int | None = None, - memory_mbytes: int | None = None, - timeout: timedelta | None = None, - restart_on_error: bool | None = None, -) -> dict: - """Build task options dictionary.""" - return { - 'build': build, - 'maxItems': max_items, - 'memoryMbytes': memory_mbytes, - 'timeoutSecs': to_seconds(timeout, as_int=True), - 'restartOnError': restart_on_error, - } - - -def build_example_run_input_dict( - *, - body: Any = None, - content_type: str | None = None, -) -> dict: - """Build example run input dictionary for Actor.""" - return { - 'body': body, - 'contentType': content_type, - } - - -def build_webhook_condition_dict( - *, - actor_id: str | None = None, - actor_task_id: str | None = None, - actor_run_id: str | None = None, -) -> dict: - """Build webhook condition dictionary.""" - return { - 'actorRunId': actor_run_id, - 'actorTaskId': actor_task_id, - 'actorId': actor_id, - } - - -def get_actor_repr( - *, - name: str | None, - title: str | None = None, - description: str | None = None, - seo_title: str | None = None, - seo_description: str | None = None, - versions: list[dict] | None = None, - restart_on_error: bool | None = None, - is_public: bool | None = None, - is_deprecated: bool | None = None, - is_anonymously_runnable: bool | None = None, - categories: list[str] | None = None, - default_run_build: str | None = None, - default_run_max_items: int | None = None, - default_run_memory_mbytes: int | None = None, - default_run_timeout: timedelta | None = None, - default_run_force_permission_level: ActorPermissionLevel | None = None, - example_run_input_body: Any = None, - example_run_input_content_type: str | None = None, - actor_standby_is_enabled: bool | None = None, - actor_standby_desired_requests_per_actor_run: int | None = None, - actor_standby_max_requests_per_actor_run: int | None = None, - actor_standby_idle_timeout: timedelta | None = None, - actor_standby_build: str | None = None, - actor_standby_memory_mbytes: int | None = None, - pricing_infos: list[dict] | None = None, - actor_permission_level: ActorPermissionLevel | None = None, - tagged_builds: dict[str, None | dict[str, str]] | None = None, -) -> dict: - """Get dictionary representation of the Actor.""" - actor_dict: dict[str, Any] = { - 'name': name, - 'title': title, - 'description': description, - 'seoTitle': seo_title, - 'seoDescription': seo_description, - 'versions': versions, - 'isPublic': is_public, - 'isDeprecated': is_deprecated, - 'isAnonymouslyRunnable': is_anonymously_runnable, - 'categories': categories, - 'pricingInfos': pricing_infos, - 'actorPermissionLevel': actor_permission_level, - 'defaultRunOptions': build_default_run_options_dict( - build=default_run_build, - max_items=default_run_max_items, - memory_mbytes=default_run_memory_mbytes, - timeout=default_run_timeout, - restart_on_error=restart_on_error, - force_permission_level=default_run_force_permission_level, - ), - 'actorStandby': build_actor_standby_dict( - is_enabled=actor_standby_is_enabled, - desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, - max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, - idle_timeout=actor_standby_idle_timeout, - build=actor_standby_build, - memory_mbytes=actor_standby_memory_mbytes, - ), - 'exampleRunInput': build_example_run_input_dict( - body=example_run_input_body, - content_type=example_run_input_content_type, - ), - } - - # Include taggedBuilds if provided - if tagged_builds is not None: - actor_dict['taggedBuilds'] = tagged_builds - - return actor_dict - - -def get_task_repr( - *, - actor_id: str | None = None, - name: str | None = None, - task_input: dict | None = None, - build: str | None = None, - max_items: int | None = None, - memory_mbytes: int | None = None, - timeout: timedelta | 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, - actor_standby_idle_timeout: timedelta | None = None, - actor_standby_build: str | None = None, - actor_standby_memory_mbytes: int | None = None, - restart_on_error: bool | None = None, -) -> dict: - """Get the dictionary representation of a task.""" - return { - 'actId': actor_id, - 'name': name, - 'title': title, - 'input': task_input, - 'options': build_task_options_dict( - build=build, - max_items=max_items, - memory_mbytes=memory_mbytes, - timeout=timeout, - restart_on_error=restart_on_error, - ), - 'actorStandby': build_actor_standby_dict( - desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, - max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, - idle_timeout=actor_standby_idle_timeout, - build=actor_standby_build, - memory_mbytes=actor_standby_memory_mbytes, - ), - } - - -def get_actor_version_repr( - *, - version_number: str | None = None, - build_tag: str | None = None, - env_vars: list[dict] | None = None, - apply_env_vars_to_build: bool | None = None, - source_type: VersionSourceType | None = None, - source_files: list[dict] | None = None, - git_repo_url: str | None = None, - tarball_url: str | None = None, - github_gist_url: str | None = None, -) -> dict: - """Get dictionary representation of an Actor version.""" - return { - 'versionNumber': version_number, - 'buildTag': build_tag, - 'envVars': env_vars, - 'applyEnvVarsToBuild': apply_env_vars_to_build, - 'sourceType': enum_to_value(source_type), - 'sourceFiles': source_files, - 'gitRepoUrl': git_repo_url, - 'tarballUrl': tarball_url, - 'gitHubGistUrl': github_gist_url, - } - - -def get_schedule_repr( - *, - cron_expression: str | None = None, - name: str | None = None, - actions: list[dict] | None = None, - description: str | None = None, - timezone: str | None = None, - title: str | None = None, - is_enabled: bool | None = None, - is_exclusive: bool | None = None, -) -> dict: - """Get dictionary representation of a schedule.""" - return { - 'cronExpression': cron_expression, - 'isEnabled': is_enabled, - 'isExclusive': is_exclusive, - 'name': name, - 'actions': actions, - 'description': description, - 'timezone': timezone, - 'title': title, - } - - -def get_webhook_repr( - *, - event_types: list[WebhookEventType] | None = None, - request_url: str | None = None, - payload_template: str | None = None, - headers_template: str | None = None, - actor_id: str | None = None, - actor_task_id: str | None = None, - actor_run_id: str | None = None, - ignore_ssl_errors: bool | None = None, - do_not_retry: bool | None = None, - idempotency_key: str | None = None, - is_ad_hoc: bool | None = None, -) -> dict: - """Prepare webhook dictionary representation for clients.""" - webhook: dict[str, Any] = { - 'requestUrl': request_url, - 'payloadTemplate': payload_template, - 'headersTemplate': headers_template, - 'ignoreSslErrors': ignore_ssl_errors, - 'doNotRetry': do_not_retry, - 'idempotencyKey': idempotency_key, - 'isAdHoc': is_ad_hoc, - 'condition': build_webhook_condition_dict( - actor_id=actor_id, - actor_task_id=actor_task_id, - actor_run_id=actor_run_id, - ), - } - - if actor_run_id is not None and is_ad_hoc is None: - webhook['isAdHoc'] = True - - if event_types is not None: - webhook['eventTypes'] = [enum_to_value(event_type) for event_type in event_types] - - return webhook diff --git a/src/apify_client/_resource_clients/_resource_client.py b/src/apify_client/_resource_clients/_resource_client.py index 5f271ee8..d38978de 100644 --- a/src/apify_client/_resource_clients/_resource_client.py +++ b/src/apify_client/_resource_clients/_resource_client.py @@ -120,6 +120,41 @@ def _build_params(self, **kwargs: Any) -> dict: merged = {**self._default_params, **kwargs} return {k: v for k, v in merged.items() if v is not None} + @staticmethod + def _clean_json_payload(data: dict) -> dict: + """Remove None values and empty nested dicts from an API request payload. + + The Apify API ignores missing fields but may reject fields explicitly set to None. + Nested sub-models serialized by Pydantic may produce empty dicts when all their + fields are None — these are also removed. + + Uses an iterative stack-based approach, analogous to _build_params for query params. + """ + result: dict = {} + stack: list[tuple[dict, dict]] = [(data, result)] + + while stack: + source, target = stack.pop() + for key, val in source.items(): + if val is None: + continue + if isinstance(val, dict): + nested: dict = {} + target[key] = nested + stack.append((val, nested)) + else: + target[key] = val + + # Remove dicts that became empty after None filtering + def _remove_empty(d: dict) -> None: + for key in [k for k, v in d.items() if isinstance(v, dict)]: + _remove_empty(d[key]) + if not d[key]: + del d[key] + + _remove_empty(result) + return result + @docs_group('Resource clients') class ResourceClient(ResourceClientBase): @@ -171,14 +206,13 @@ def _get(self, *, timeout: timedelta | None = None) -> dict | None: catch_not_found_or_throw(exc) return None - def _update(self, updated_fields: dict, *, timeout: timedelta | None = None) -> dict: + def _update(self, **kwargs: Any) -> dict: """Perform a PUT request to update this resource with the given fields.""" response = self._http_client.call( url=self._build_url(), method='PUT', params=self._build_params(), - json=updated_fields, - timeout=timeout, + json=self._clean_json_payload(kwargs), ) return response_to_dict(response) @@ -203,13 +237,13 @@ def _list(self, **kwargs: Any) -> dict: ) return response_to_dict(response) - def _create(self, created_fields: dict) -> dict: + def _create(self, **kwargs: Any) -> dict: """Perform a POST request to create a resource.""" response = self._http_client.call( url=self._build_url(), method='POST', params=self._build_params(), - json=created_fields, + json=self._clean_json_payload(kwargs), ) return response_to_dict(response) @@ -219,7 +253,7 @@ def _get_or_create(self, *, name: str | None = None, resource_fields: dict | Non url=self._build_url(), method='POST', params=self._build_params(name=name), - json=resource_fields, + json=self._clean_json_payload(resource_fields) if resource_fields is not None else None, ) return response_to_dict(response) @@ -338,14 +372,13 @@ async def _get(self, *, timeout: timedelta | None = None) -> dict | None: catch_not_found_or_throw(exc) return None - async def _update(self, updated_fields: dict, *, timeout: timedelta | None = None) -> dict: + async def _update(self, **kwargs: Any) -> dict: """Perform a PUT request to update this resource with the given fields.""" response = await self._http_client.call( url=self._build_url(), method='PUT', params=self._build_params(), - json=updated_fields, - timeout=timeout, + json=self._clean_json_payload(kwargs), ) return response_to_dict(response) @@ -370,13 +403,13 @@ async def _list(self, **kwargs: Any) -> dict: ) return response_to_dict(response) - async def _create(self, created_fields: dict) -> dict: + async def _create(self, **kwargs: Any) -> dict: """Perform a POST request to create a resource.""" response = await self._http_client.call( url=self._build_url(), method='POST', params=self._build_params(), - json=created_fields, + json=self._clean_json_payload(kwargs), ) return response_to_dict(response) @@ -386,7 +419,7 @@ async def _get_or_create(self, *, name: str | None = None, resource_fields: dict url=self._build_url(), method='POST', params=self._build_params(name=name), - json=resource_fields, + json=self._clean_json_payload(resource_fields) if resource_fields is not None else None, ) return response_to_dict(response) diff --git a/src/apify_client/_resource_clients/actor.py b/src/apify_client/_resource_clients/actor.py index c7000e66..45b3c370 100644 --- a/src/apify_client/_resource_clients/actor.py +++ b/src/apify_client/_resource_clients/actor.py @@ -2,24 +2,32 @@ from typing import TYPE_CHECKING, Any, Literal +from pydantic import TypeAdapter + from apify_client._docs import docs_group from apify_client._models import ( Actor, ActorPermissionLevel, ActorResponse, + ActorStandby, Build, BuildResponse, + CreateOrUpdateVersionRequest, + DefaultRunOptions, + ExampleRunInput, + FlatPricePerMonthActorPricingInfo, + FreeActorPricingInfo, + PayPerEventActorPricingInfo, + PricePerDatasetItemActorPricingInfo, Run, RunOrigin, RunResponse, + UpdateActorRequest, ) -from apify_client._representations import get_actor_repr 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, - enum_to_value, - filter_none_values, response_to_dict, to_seconds, ) @@ -47,6 +55,14 @@ WebhookCollectionClientAsync, ) +_PricingInfo = ( + PayPerEventActorPricingInfo + | PricePerDatasetItemActorPricingInfo + | FlatPricePerMonthActorPricingInfo + | FreeActorPricingInfo +) +_pricing_info_list_adapter = TypeAdapter(list[_PricingInfo]) + @docs_group('Resource clients') class ActorClient(ResourceClient): @@ -90,11 +106,10 @@ def update( description: str | None = None, seo_title: str | None = None, seo_description: str | None = None, - versions: list[dict] | None = None, + versions: list[dict[str, Any]] | None = None, restart_on_error: bool | None = None, is_public: bool | None = None, is_deprecated: bool | None = None, - is_anonymously_runnable: bool | None = None, categories: list[str] | None = None, default_run_build: str | None = None, default_run_max_items: int | None = None, @@ -108,7 +123,7 @@ def update( actor_standby_idle_timeout: timedelta | None = None, actor_standby_build: str | None = None, actor_standby_memory_mbytes: int | None = None, - pricing_infos: list[dict] | None = None, + pricing_infos: list[dict[str, Any]] | None = None, actor_permission_level: ActorPermissionLevel | None = None, tagged_builds: dict[str, None | dict[str, str]] | None = None, ) -> Actor: @@ -127,7 +142,6 @@ def update( a non-zero status code. is_public: Whether the Actor is public. is_deprecated: Whether the Actor is deprecated. - is_anonymously_runnable: Whether the Actor is anonymously runnable. categories: The categories to which the Actor belongs to. default_run_build: Tag or number of the build that you want to run by default. default_run_max_items: Default limit of the number of results that will be returned @@ -154,37 +168,40 @@ def update( Returns: The updated Actor. """ - actor_representation = get_actor_repr( + request = UpdateActorRequest( name=name, title=title, description=description, seo_title=seo_title, seo_description=seo_description, - versions=versions, - restart_on_error=restart_on_error, + versions=[CreateOrUpdateVersionRequest.model_validate(v) for v in versions] if versions else None, is_public=is_public, is_deprecated=is_deprecated, - is_anonymously_runnable=is_anonymously_runnable, categories=categories, - default_run_build=default_run_build, - default_run_max_items=default_run_max_items, - default_run_memory_mbytes=default_run_memory_mbytes, - default_run_timeout=default_run_timeout, - example_run_input_body=example_run_input_body, - example_run_input_content_type=example_run_input_content_type, - actor_standby_is_enabled=actor_standby_is_enabled, - actor_standby_desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, - actor_standby_max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, - actor_standby_idle_timeout=actor_standby_idle_timeout, - actor_standby_build=actor_standby_build, - actor_standby_memory_mbytes=actor_standby_memory_mbytes, - pricing_infos=pricing_infos, + pricing_infos=_pricing_info_list_adapter.validate_python(pricing_infos) if pricing_infos else None, actor_permission_level=actor_permission_level, + default_run_options=DefaultRunOptions( + build=default_run_build, + max_items=default_run_max_items, + memory_mbytes=default_run_memory_mbytes, + timeout_secs=to_seconds(default_run_timeout, as_int=True), + restart_on_error=restart_on_error, + ), + actor_standby=ActorStandby( + is_enabled=actor_standby_is_enabled, + desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, + max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, + idle_timeout_secs=to_seconds(actor_standby_idle_timeout, as_int=True), + build=actor_standby_build, + memory_mbytes=actor_standby_memory_mbytes, + ), + example_run_input=ExampleRunInput( + body=example_run_input_body, + content_type=example_run_input_content_type, + ), tagged_builds=tagged_builds, ) - cleaned = filter_none_values(actor_representation, remove_empty_dicts=True) - - result = self._update(cleaned) + result = self._update(**request.model_dump(by_alias=True, exclude_none=True)) return ActorResponse.model_validate(result).data def delete(self) -> None: @@ -458,8 +475,8 @@ def last_run( resource_id='last', resource_path='runs', params=self._build_params( - status=enum_to_value(status), - origin=enum_to_value(origin), + status=status, + origin=origin, ), **self._base_client_kwargs, ) @@ -554,11 +571,10 @@ async def update( description: str | None = None, seo_title: str | None = None, seo_description: str | None = None, - versions: list[dict] | None = None, + versions: list[dict[str, Any]] | None = None, restart_on_error: bool | None = None, is_public: bool | None = None, is_deprecated: bool | None = None, - is_anonymously_runnable: bool | None = None, categories: list[str] | None = None, default_run_build: str | None = None, default_run_max_items: int | None = None, @@ -572,7 +588,7 @@ async def update( actor_standby_idle_timeout: timedelta | None = None, actor_standby_build: str | None = None, actor_standby_memory_mbytes: int | None = None, - pricing_infos: list[dict] | None = None, + pricing_infos: list[dict[str, Any]] | None = None, actor_permission_level: ActorPermissionLevel | None = None, tagged_builds: dict[str, None | dict[str, str]] | None = None, ) -> Actor: @@ -591,7 +607,6 @@ async def update( a non-zero status code. is_public: Whether the Actor is public. is_deprecated: Whether the Actor is deprecated. - is_anonymously_runnable: Whether the Actor is anonymously runnable. categories: The categories to which the Actor belongs to. default_run_build: Tag or number of the build that you want to run by default. default_run_max_items: Default limit of the number of results that will be returned @@ -618,37 +633,40 @@ async def update( Returns: The updated Actor. """ - actor_representation = get_actor_repr( + request = UpdateActorRequest( name=name, title=title, description=description, seo_title=seo_title, seo_description=seo_description, - versions=versions, - restart_on_error=restart_on_error, + versions=[CreateOrUpdateVersionRequest.model_validate(v) for v in versions] if versions else None, is_public=is_public, is_deprecated=is_deprecated, - is_anonymously_runnable=is_anonymously_runnable, categories=categories, - default_run_build=default_run_build, - default_run_max_items=default_run_max_items, - default_run_memory_mbytes=default_run_memory_mbytes, - default_run_timeout=default_run_timeout, - example_run_input_body=example_run_input_body, - example_run_input_content_type=example_run_input_content_type, - actor_standby_is_enabled=actor_standby_is_enabled, - actor_standby_desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, - actor_standby_max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, - actor_standby_idle_timeout=actor_standby_idle_timeout, - actor_standby_build=actor_standby_build, - actor_standby_memory_mbytes=actor_standby_memory_mbytes, - pricing_infos=pricing_infos, + pricing_infos=_pricing_info_list_adapter.validate_python(pricing_infos) if pricing_infos else None, actor_permission_level=actor_permission_level, + default_run_options=DefaultRunOptions( + build=default_run_build, + max_items=default_run_max_items, + memory_mbytes=default_run_memory_mbytes, + timeout_secs=to_seconds(default_run_timeout, as_int=True), + restart_on_error=restart_on_error, + ), + actor_standby=ActorStandby( + is_enabled=actor_standby_is_enabled, + desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, + max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, + idle_timeout_secs=to_seconds(actor_standby_idle_timeout, as_int=True), + build=actor_standby_build, + memory_mbytes=actor_standby_memory_mbytes, + ), + example_run_input=ExampleRunInput( + body=example_run_input_body, + content_type=example_run_input_content_type, + ), tagged_builds=tagged_builds, ) - cleaned = filter_none_values(actor_representation, remove_empty_dicts=True) - - result = await self._update(cleaned) + result = await self._update(**request.model_dump(by_alias=True, exclude_none=True)) return ActorResponse.model_validate(result).data async def delete(self) -> None: @@ -930,8 +948,8 @@ def last_run( resource_id='last', resource_path='runs', params=self._build_params( - status=enum_to_value(status), - origin=enum_to_value(origin), + status=status, + origin=origin, ), **self._base_client_kwargs, ) diff --git a/src/apify_client/_resource_clients/actor_collection.py b/src/apify_client/_resource_clients/actor_collection.py index 255d9f9c..1ea01ea8 100644 --- a/src/apify_client/_resource_clients/actor_collection.py +++ b/src/apify_client/_resource_clients/actor_collection.py @@ -3,10 +3,18 @@ from typing import TYPE_CHECKING, Any, Literal from apify_client._docs import docs_group -from apify_client._models import Actor, ActorResponse, ListOfActors, ListOfActorsResponse -from apify_client._representations import get_actor_repr +from apify_client._models import ( + Actor, + ActorResponse, + ActorStandby, + CreateActorRequest, + DefaultRunOptions, + ExampleRunInput, + ListOfActors, + ListOfActorsResponse, +) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._utils import filter_none_values +from apify_client._utils import to_seconds if TYPE_CHECKING: from datetime import timedelta @@ -58,11 +66,10 @@ def create( description: str | None = None, seo_title: str | None = None, seo_description: str | None = None, - versions: list[dict] | None = None, # ty: ignore[invalid-type-form] + versions: list[dict[str, Any]] | None = None, # ty: ignore[invalid-type-form] restart_on_error: bool | None = None, is_public: bool | None = None, is_deprecated: bool | None = None, - is_anonymously_runnable: bool | None = None, categories: list[str] | None = None, # ty: ignore[invalid-type-form] default_run_build: str | None = None, default_run_max_items: int | None = None, @@ -92,7 +99,6 @@ def create( a non-zero status code. is_public: Whether the Actor is public. is_deprecated: Whether the Actor is deprecated. - is_anonymously_runnable: Whether the Actor is anonymously runnable. categories: The categories to which the Actor belongs to. default_run_build: Tag or number of the build that you want to run by default. default_run_max_items: Default limit of the number of results that will be returned by runs @@ -114,33 +120,37 @@ def create( Returns: The created Actor. """ - actor_representation = get_actor_repr( + request = CreateActorRequest( name=name, title=title, description=description, seo_title=seo_title, seo_description=seo_description, versions=versions, - restart_on_error=restart_on_error, is_public=is_public, is_deprecated=is_deprecated, - is_anonymously_runnable=is_anonymously_runnable, categories=categories, - default_run_build=default_run_build, - default_run_max_items=default_run_max_items, - default_run_memory_mbytes=default_run_memory_mbytes, - default_run_timeout=default_run_timeout, - example_run_input_body=example_run_input_body, - example_run_input_content_type=example_run_input_content_type, - actor_standby_is_enabled=actor_standby_is_enabled, - actor_standby_desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, - actor_standby_max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, - actor_standby_idle_timeout=actor_standby_idle_timeout, - actor_standby_build=actor_standby_build, - actor_standby_memory_mbytes=actor_standby_memory_mbytes, + default_run_options=DefaultRunOptions( + build=default_run_build, + max_items=default_run_max_items, + memory_mbytes=default_run_memory_mbytes, + timeout_secs=to_seconds(default_run_timeout, as_int=True), + restart_on_error=restart_on_error, + ), + actor_standby=ActorStandby( + is_enabled=actor_standby_is_enabled, + desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, + max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, + idle_timeout_secs=to_seconds(actor_standby_idle_timeout, as_int=True), + build=actor_standby_build, + memory_mbytes=actor_standby_memory_mbytes, + ), + example_run_input=ExampleRunInput( + body=example_run_input_body, + content_type=example_run_input_content_type, + ), ) - - result = self._create(filter_none_values(actor_representation, remove_empty_dicts=True)) + result = self._create(**request.model_dump(by_alias=True, exclude_none=True)) return ActorResponse.model_validate(result).data @@ -190,11 +200,10 @@ async def create( description: str | None = None, seo_title: str | None = None, seo_description: str | None = None, - versions: list[dict] | None = None, # ty: ignore[invalid-type-form] + versions: list[dict[str, Any]] | None = None, # ty: ignore[invalid-type-form] restart_on_error: bool | None = None, is_public: bool | None = None, is_deprecated: bool | None = None, - is_anonymously_runnable: bool | None = None, categories: list[str] | None = None, # ty: ignore[invalid-type-form] default_run_build: str | None = None, default_run_max_items: int | None = None, @@ -224,7 +233,6 @@ async def create( a non-zero status code. is_public: Whether the Actor is public. is_deprecated: Whether the Actor is deprecated. - is_anonymously_runnable: Whether the Actor is anonymously runnable. categories: The categories to which the Actor belongs to. default_run_build: Tag or number of the build that you want to run by default. default_run_max_items: Default limit of the number of results that will be returned by runs @@ -246,31 +254,35 @@ async def create( Returns: The created Actor. """ - actor_representation = get_actor_repr( + request = CreateActorRequest( name=name, title=title, description=description, seo_title=seo_title, seo_description=seo_description, versions=versions, - restart_on_error=restart_on_error, is_public=is_public, is_deprecated=is_deprecated, - is_anonymously_runnable=is_anonymously_runnable, categories=categories, - default_run_build=default_run_build, - default_run_max_items=default_run_max_items, - default_run_memory_mbytes=default_run_memory_mbytes, - default_run_timeout=default_run_timeout, - example_run_input_body=example_run_input_body, - example_run_input_content_type=example_run_input_content_type, - actor_standby_is_enabled=actor_standby_is_enabled, - actor_standby_desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, - actor_standby_max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, - actor_standby_idle_timeout=actor_standby_idle_timeout, - actor_standby_build=actor_standby_build, - actor_standby_memory_mbytes=actor_standby_memory_mbytes, + default_run_options=DefaultRunOptions( + build=default_run_build, + max_items=default_run_max_items, + memory_mbytes=default_run_memory_mbytes, + timeout_secs=to_seconds(default_run_timeout, as_int=True), + restart_on_error=restart_on_error, + ), + actor_standby=ActorStandby( + is_enabled=actor_standby_is_enabled, + desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, + max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, + idle_timeout_secs=to_seconds(actor_standby_idle_timeout, as_int=True), + build=actor_standby_build, + memory_mbytes=actor_standby_memory_mbytes, + ), + example_run_input=ExampleRunInput( + body=example_run_input_body, + content_type=example_run_input_content_type, + ), ) - - result = await self._create(filter_none_values(actor_representation, remove_empty_dicts=True)) + result = await self._create(**request.model_dump(by_alias=True, exclude_none=True)) return ActorResponse.model_validate(result).data diff --git a/src/apify_client/_resource_clients/actor_env_var.py b/src/apify_client/_resource_clients/actor_env_var.py index ea5acce9..f14b3f8d 100644 --- a/src/apify_client/_resource_clients/actor_env_var.py +++ b/src/apify_client/_resource_clients/actor_env_var.py @@ -5,21 +5,6 @@ from apify_client._docs import docs_group from apify_client._models import EnvVar, EnvVarResponse from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._utils import filter_none_values - - -def get_actor_env_var_representation( - *, - is_secret: bool | None = None, - name: str | None = None, - value: str | None = None, -) -> dict: - """Return an environment variable representation of the Actor in a dictionary.""" - return { - 'isSecret': is_secret, - 'name': name, - 'value': value, - } @docs_group('Resource clients') @@ -66,13 +51,9 @@ def update( Returns: The updated Actor environment variable. """ - actor_env_var_representation = get_actor_env_var_representation( - is_secret=is_secret, - name=name, - value=value, + result = self._update( + **EnvVar(name=name, value=value, is_secret=is_secret).model_dump(by_alias=True, exclude_none=True) ) - cleaned = filter_none_values(actor_env_var_representation) - result = self._update(cleaned) return EnvVarResponse.model_validate(result).data def delete(self) -> None: @@ -127,13 +108,9 @@ async def update( Returns: The updated Actor environment variable. """ - actor_env_var_representation = get_actor_env_var_representation( - is_secret=is_secret, - name=name, - value=value, + result = await self._update( + **EnvVar(name=name, value=value, is_secret=is_secret).model_dump(by_alias=True, exclude_none=True) ) - cleaned = filter_none_values(actor_env_var_representation) - result = await self._update(cleaned) return EnvVarResponse.model_validate(result).data async def delete(self) -> None: diff --git a/src/apify_client/_resource_clients/actor_env_var_collection.py b/src/apify_client/_resource_clients/actor_env_var_collection.py index 4a82fb6a..5462921a 100644 --- a/src/apify_client/_resource_clients/actor_env_var_collection.py +++ b/src/apify_client/_resource_clients/actor_env_var_collection.py @@ -5,8 +5,6 @@ from apify_client._docs import docs_group from apify_client._models import EnvVar, EnvVarResponse, ListOfEnvVars, ListOfEnvVarsResponse from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._resource_clients.actor_env_var import get_actor_env_var_representation -from apify_client._utils import filter_none_values @docs_group('Resource clients') @@ -51,13 +49,9 @@ def create( Returns: The created Actor environment variable. """ - actor_env_var_representation = get_actor_env_var_representation( - is_secret=is_secret, - name=name, - value=value, + result = self._create( + **EnvVar(name=name, value=value, is_secret=is_secret).model_dump(by_alias=True, exclude_none=True) ) - - result = self._create(filter_none_values(actor_env_var_representation)) return EnvVarResponse.model_validate(result).data @@ -103,11 +97,7 @@ async def create( Returns: The created Actor environment variable. """ - actor_env_var_representation = get_actor_env_var_representation( - is_secret=is_secret, - name=name, - value=value, + result = await self._create( + **EnvVar(name=name, value=value, is_secret=is_secret).model_dump(by_alias=True, exclude_none=True) ) - - result = await self._create(filter_none_values(actor_env_var_representation)) return EnvVarResponse.model_validate(result).data diff --git a/src/apify_client/_resource_clients/actor_version.py b/src/apify_client/_resource_clients/actor_version.py index ce53e521..9fac9ba7 100644 --- a/src/apify_client/_resource_clients/actor_version.py +++ b/src/apify_client/_resource_clients/actor_version.py @@ -2,11 +2,19 @@ from typing import TYPE_CHECKING, Any +from pydantic import TypeAdapter + from apify_client._docs import docs_group -from apify_client._models import Version, VersionResponse, VersionSourceType -from apify_client._representations import get_actor_version_repr +from apify_client._models import ( + CreateOrUpdateVersionRequest, + EnvVar, + SourceCodeFile, + SourceCodeFolder, + Version, + VersionResponse, + VersionSourceType, +) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._utils import filter_none_values if TYPE_CHECKING: from apify_client._resource_clients import ( @@ -16,6 +24,8 @@ ActorEnvVarCollectionClientAsync, ) +_source_file_list_adapter = TypeAdapter(list[SourceCodeFile | SourceCodeFolder]) + @docs_group('Resource clients') class ActorVersionClient(ResourceClient): @@ -55,10 +65,10 @@ def update( self, *, build_tag: str | None = None, - env_vars: list[dict] | None = None, + env_vars: list[dict[str, Any]] | None = None, apply_env_vars_to_build: bool | None = None, source_type: VersionSourceType | None = None, - source_files: list[dict] | None = None, + source_files: list[dict[str, Any]] | None = None, git_repo_url: str | None = None, tarball_url: str | None = None, github_gist_url: str | None = None, @@ -86,19 +96,17 @@ def update( Returns: The updated Actor version. """ - actor_version_representation = get_actor_version_repr( + request = CreateOrUpdateVersionRequest( build_tag=build_tag, - env_vars=env_vars, + env_vars=[EnvVar.model_validate(v) for v in env_vars] if env_vars else None, apply_env_vars_to_build=apply_env_vars_to_build, source_type=source_type, - source_files=source_files, + source_files=_source_file_list_adapter.validate_python(source_files) if source_files else None, git_repo_url=git_repo_url, tarball_url=tarball_url, github_gist_url=github_gist_url, ) - cleaned = filter_none_values(actor_version_representation) - - result = self._update(cleaned) + result = self._update(**request.model_dump(by_alias=True, exclude_none=True)) return VersionResponse.model_validate(result).data def delete(self) -> None: @@ -165,10 +173,10 @@ async def update( self, *, build_tag: str | None = None, - env_vars: list[dict] | None = None, + env_vars: list[dict[str, Any]] | None = None, apply_env_vars_to_build: bool | None = None, source_type: VersionSourceType | None = None, - source_files: list[dict] | None = None, + source_files: list[dict[str, Any]] | None = None, git_repo_url: str | None = None, tarball_url: str | None = None, github_gist_url: str | None = None, @@ -196,19 +204,17 @@ async def update( Returns: The updated Actor version. """ - actor_version_representation = get_actor_version_repr( + request = CreateOrUpdateVersionRequest( build_tag=build_tag, - env_vars=env_vars, + env_vars=[EnvVar.model_validate(v) for v in env_vars] if env_vars else None, apply_env_vars_to_build=apply_env_vars_to_build, source_type=source_type, - source_files=source_files, + source_files=_source_file_list_adapter.validate_python(source_files) if source_files else None, git_repo_url=git_repo_url, tarball_url=tarball_url, github_gist_url=github_gist_url, ) - cleaned = filter_none_values(actor_version_representation) - - result = await self._update(cleaned) + result = await self._update(**request.model_dump(by_alias=True, exclude_none=True)) return VersionResponse.model_validate(result).data async def delete(self) -> None: diff --git a/src/apify_client/_resource_clients/actor_version_collection.py b/src/apify_client/_resource_clients/actor_version_collection.py index bde7517b..edecd406 100644 --- a/src/apify_client/_resource_clients/actor_version_collection.py +++ b/src/apify_client/_resource_clients/actor_version_collection.py @@ -2,17 +2,23 @@ from typing import Any +from pydantic import TypeAdapter + from apify_client._docs import docs_group from apify_client._models import ( + CreateOrUpdateVersionRequest, + EnvVar, ListOfVersions, ListOfVersionsResponse, + SourceCodeFile, + SourceCodeFolder, Version, VersionResponse, VersionSourceType, ) -from apify_client._representations import get_actor_version_repr from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._utils import filter_none_values + +_source_file_list_adapter = TypeAdapter(list[SourceCodeFile | SourceCodeFolder]) @docs_group('Resource clients') @@ -43,10 +49,10 @@ def create( *, version_number: str, build_tag: str | None = None, - env_vars: list[dict] | None = None, # ty: ignore[invalid-type-form] + env_vars: list[dict[str, Any]] | None = None, # ty: ignore[invalid-type-form] apply_env_vars_to_build: bool | None = None, source_type: VersionSourceType, - source_files: list[dict] | None = None, # ty: ignore[invalid-type-form] + source_files: list[dict[str, Any]] | None = None, # ty: ignore[invalid-type-form] git_repo_url: str | None = None, tarball_url: str | None = None, github_gist_url: str | None = None, @@ -75,19 +81,18 @@ def create( Returns: The created Actor version. """ - actor_version_representation = get_actor_version_repr( + request = CreateOrUpdateVersionRequest( version_number=version_number, build_tag=build_tag, - env_vars=env_vars, + env_vars=[EnvVar.model_validate(v) for v in env_vars] if env_vars else None, apply_env_vars_to_build=apply_env_vars_to_build, source_type=source_type, - source_files=source_files, + source_files=_source_file_list_adapter.validate_python(source_files) if source_files else None, git_repo_url=git_repo_url, tarball_url=tarball_url, github_gist_url=github_gist_url, ) - - result = self._create(filter_none_values(actor_version_representation)) + result = self._create(**request.model_dump(by_alias=True, exclude_none=True)) return VersionResponse.model_validate(result).data @@ -119,10 +124,10 @@ async def create( *, version_number: str, build_tag: str | None = None, - env_vars: list[dict] | None = None, # ty: ignore[invalid-type-form] + env_vars: list[dict[str, Any]] | None = None, # ty: ignore[invalid-type-form] apply_env_vars_to_build: bool | None = None, source_type: VersionSourceType, - source_files: list[dict] | None = None, # ty: ignore[invalid-type-form] + source_files: list[dict[str, Any]] | None = None, # ty: ignore[invalid-type-form] git_repo_url: str | None = None, tarball_url: str | None = None, github_gist_url: str | None = None, @@ -151,17 +156,16 @@ async def create( Returns: The created Actor version. """ - actor_version_representation = get_actor_version_repr( + request = CreateOrUpdateVersionRequest( version_number=version_number, build_tag=build_tag, - env_vars=env_vars, + env_vars=[EnvVar.model_validate(v) for v in env_vars] if env_vars else None, apply_env_vars_to_build=apply_env_vars_to_build, source_type=source_type, - source_files=source_files, + source_files=_source_file_list_adapter.validate_python(source_files) if source_files else None, git_repo_url=git_repo_url, tarball_url=tarball_url, github_gist_url=github_gist_url, ) - - result = await self._create(filter_none_values(actor_version_representation)) + result = await self._create(**request.model_dump(by_alias=True, exclude_none=True)) return VersionResponse.model_validate(result).data diff --git a/src/apify_client/_resource_clients/dataset.py b/src/apify_client/_resource_clients/dataset.py index 2720a6cc..975b15e8 100644 --- a/src/apify_client/_resource_clients/dataset.py +++ b/src/apify_client/_resource_clients/dataset.py @@ -13,7 +13,6 @@ from apify_client._utils import ( catch_not_found_or_throw, create_storage_content_signature, - filter_none_values, response_to_dict, response_to_list, ) @@ -95,13 +94,7 @@ def update(self, *, name: str | None = None, general_access: GeneralAccess | Non Returns: The updated dataset. """ - updated_fields = { - 'name': name, - 'generalAccess': general_access, - } - cleaned = filter_none_values(updated_fields) - - result = self._update(cleaned, timeout=FAST_OPERATION_TIMEOUT) + result = self._update(name=name, generalAccess=general_access) return DatasetResponse.model_validate(result).data def delete(self) -> None: @@ -716,13 +709,7 @@ async def update(self, *, name: str | None = None, general_access: GeneralAccess Returns: The updated dataset. """ - updated_fields = { - 'name': name, - 'generalAccess': general_access, - } - cleaned = filter_none_values(updated_fields) - - result = await self._update(cleaned, timeout=FAST_OPERATION_TIMEOUT) + result = await self._update(name=name, generalAccess=general_access) return DatasetResponse.model_validate(result).data async def delete(self) -> None: diff --git a/src/apify_client/_resource_clients/dataset_collection.py b/src/apify_client/_resource_clients/dataset_collection.py index 2322fd67..be303b82 100644 --- a/src/apify_client/_resource_clients/dataset_collection.py +++ b/src/apify_client/_resource_clients/dataset_collection.py @@ -5,7 +5,6 @@ from apify_client._docs import docs_group from apify_client._models import Dataset, DatasetResponse, ListOfDatasets, ListOfDatasetsResponse from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._utils import filter_none_values @docs_group('Resource clients') @@ -56,7 +55,7 @@ def get_or_create(self, *, name: str | None = None, schema: dict | None = None) Returns: The retrieved or newly-created dataset. """ - result = self._get_or_create(name=name, resource_fields=filter_none_values({'schema': schema})) + result = self._get_or_create(name=name, resource_fields={'schema': schema}) return DatasetResponse.model_validate(result).data @@ -113,5 +112,5 @@ async def get_or_create( Returns: The retrieved or newly-created dataset. """ - result = await self._get_or_create(name=name, resource_fields=filter_none_values({'schema': schema})) + result = await self._get_or_create(name=name, resource_fields={'schema': schema}) return DatasetResponse.model_validate(result).data diff --git a/src/apify_client/_resource_clients/key_value_store.py b/src/apify_client/_resource_clients/key_value_store.py index 03119c5f..af3be937 100644 --- a/src/apify_client/_resource_clients/key_value_store.py +++ b/src/apify_client/_resource_clients/key_value_store.py @@ -21,7 +21,6 @@ create_hmac_signature, create_storage_content_signature, encode_key_value_store_record_value, - filter_none_values, response_to_dict, ) from apify_client.errors import ApifyApiError, InvalidResponseBodyError @@ -106,13 +105,7 @@ def update(self, *, name: str | None = None, general_access: GeneralAccess | Non Returns: The updated key-value store. """ - updated_fields = { - 'name': name, - 'generalAccess': general_access, - } - cleaned = filter_none_values(updated_fields) - - result = self._update(cleaned, timeout=FAST_OPERATION_TIMEOUT) + result = self._update(name=name, generalAccess=general_access) return KeyValueStoreResponse.model_validate(result).data def delete(self) -> None: @@ -495,13 +488,7 @@ async def update( Returns: The updated key-value store. """ - updated_fields = { - 'name': name, - 'generalAccess': general_access, - } - cleaned = filter_none_values(updated_fields) - - result = await self._update(cleaned, timeout=FAST_OPERATION_TIMEOUT) + result = await self._update(name=name, generalAccess=general_access) return KeyValueStoreResponse.model_validate(result).data async def delete(self) -> None: diff --git a/src/apify_client/_resource_clients/key_value_store_collection.py b/src/apify_client/_resource_clients/key_value_store_collection.py index f693952d..37691eb1 100644 --- a/src/apify_client/_resource_clients/key_value_store_collection.py +++ b/src/apify_client/_resource_clients/key_value_store_collection.py @@ -10,7 +10,6 @@ ListOfKeyValueStoresResponse, ) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._utils import filter_none_values @docs_group('Resource clients') @@ -66,7 +65,7 @@ def get_or_create( Returns: The retrieved or newly-created key-value store. """ - result = self._get_or_create(name=name, resource_fields=filter_none_values({'schema': schema})) + result = self._get_or_create(name=name, resource_fields={'schema': schema}) return KeyValueStoreResponse.model_validate(result).data @@ -123,5 +122,5 @@ async def get_or_create( Returns: The retrieved or newly-created key-value store. """ - result = await self._get_or_create(name=name, resource_fields=filter_none_values({'schema': schema})) + result = await self._get_or_create(name=name, resource_fields={'schema': schema}) return KeyValueStoreResponse.model_validate(result).data diff --git a/src/apify_client/_resource_clients/request_queue.py b/src/apify_client/_resource_clients/request_queue.py index 408a3d73..0fe613fb 100644 --- a/src/apify_client/_resource_clients/request_queue.py +++ b/src/apify_client/_resource_clients/request_queue.py @@ -36,7 +36,7 @@ UnlockRequestsResult, ) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._utils import catch_not_found_or_throw, filter_none_values, response_to_dict, to_seconds +from apify_client._utils import catch_not_found_or_throw, response_to_dict, to_seconds from apify_client.errors import ApifyApiError if TYPE_CHECKING: @@ -100,13 +100,7 @@ def update(self, *, name: str | None = None, general_access: GeneralAccess | Non Returns: The updated request queue. """ - updated_fields = { - 'name': name, - 'generalAccess': general_access, - } - cleaned = filter_none_values(updated_fields) - - result = self._update(cleaned, timeout=FAST_OPERATION_TIMEOUT) + result = self._update(name=name, generalAccess=general_access) return RequestQueueResponse.model_validate(result).data def delete(self) -> None: @@ -515,13 +509,7 @@ async def update( Returns: The updated request queue. """ - updated_fields = { - 'name': name, - 'generalAccess': general_access, - } - cleaned = filter_none_values(updated_fields) - - result = await self._update(cleaned, timeout=FAST_OPERATION_TIMEOUT) + result = await self._update(name=name, generalAccess=general_access) return RequestQueueResponse.model_validate(result).data async def delete(self) -> None: diff --git a/src/apify_client/_resource_clients/run.py b/src/apify_client/_resource_clients/run.py index 1f7da905..255ae095 100644 --- a/src/apify_client/_resource_clients/run.py +++ b/src/apify_client/_resource_clients/run.py @@ -15,7 +15,6 @@ from apify_client._streamed_log import StreamedLog, StreamedLogAsync from apify_client._utils import ( encode_key_value_store_record_value, - filter_none_values, response_to_dict, to_safe_id, to_seconds, @@ -91,14 +90,11 @@ def update( Returns: The updated run. """ - updated_fields = { - 'statusMessage': status_message, - 'isStatusMessageTerminal': is_status_message_terminal, - 'generalAccess': general_access, - } - cleaned = filter_none_values(updated_fields) - - result = self._update(cleaned) + result = self._update( + statusMessage=status_message, + isStatusMessageTerminal=is_status_message_terminal, + generalAccess=general_access, + ) return RunResponse.model_validate(result).data def delete(self) -> None: @@ -485,14 +481,11 @@ async def update( Returns: The updated run. """ - updated_fields = { - 'statusMessage': status_message, - 'isStatusMessageTerminal': is_status_message_terminal, - 'generalAccess': general_access, - } - cleaned = filter_none_values(updated_fields) - - result = await self._update(cleaned) + result = await self._update( + statusMessage=status_message, + isStatusMessageTerminal=is_status_message_terminal, + generalAccess=general_access, + ) return RunResponse.model_validate(result).data async def abort(self, *, gracefully: bool | None = None) -> Run: diff --git a/src/apify_client/_resource_clients/run_collection.py b/src/apify_client/_resource_clients/run_collection.py index 2cf32801..ab7a1a20 100644 --- a/src/apify_client/_resource_clients/run_collection.py +++ b/src/apify_client/_resource_clients/run_collection.py @@ -5,7 +5,6 @@ from apify_client._docs import docs_group from apify_client._models import ListOfRuns, ListOfRunsResponse from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._utils import enum_to_value if TYPE_CHECKING: from datetime import datetime @@ -54,7 +53,7 @@ def list( Returns: The retrieved Actor runs. """ - status_param = [enum_to_value(s) for s in status] if isinstance(status, list) else enum_to_value(status) + status_param = list(status) if isinstance(status, list) else status result = self._list( limit=limit, @@ -108,7 +107,7 @@ async def list( Returns: The retrieved Actor runs. """ - status_param = [enum_to_value(s) for s in status] if isinstance(status, list) else enum_to_value(status) + status_param = list(status) if isinstance(status, list) else status result = await self._list( limit=limit, diff --git a/src/apify_client/_resource_clients/schedule.py b/src/apify_client/_resource_clients/schedule.py index 63bd4ebd..800ce0d4 100644 --- a/src/apify_client/_resource_clients/schedule.py +++ b/src/apify_client/_resource_clients/schedule.py @@ -3,10 +3,16 @@ from typing import Any from apify_client._docs import docs_group -from apify_client._models import Schedule, ScheduleInvoked, ScheduleLogResponse, ScheduleResponse -from apify_client._representations import get_schedule_repr +from apify_client._models import ( + Schedule, + ScheduleCreate, + ScheduleCreateActions, + ScheduleInvoked, + ScheduleLogResponse, + ScheduleResponse, +) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._utils import catch_not_found_or_throw, filter_none_values, response_to_dict +from apify_client._utils import catch_not_found_or_throw, response_to_dict from apify_client.errors import ApifyApiError @@ -42,7 +48,7 @@ def update( is_enabled: bool | None = None, is_exclusive: bool | None = None, name: str | None = None, - actions: list[dict] | None = None, + actions: list[dict[str, Any]] | None = None, description: str | None = None, timezone: str | None = None, title: str | None = None, @@ -66,19 +72,17 @@ def update( Returns: The updated schedule. """ - schedule_representation = get_schedule_repr( + request = ScheduleCreate( cron_expression=cron_expression, is_enabled=is_enabled, is_exclusive=is_exclusive, name=name, - actions=actions, + actions=[ScheduleCreateActions.model_validate(a) for a in actions] if actions else None, description=description, timezone=timezone, title=title, ) - cleaned = filter_none_values(schedule_representation) - - result = self._update(cleaned) + result = self._update(**request.model_dump(by_alias=True, exclude_none=True)) return ScheduleResponse.model_validate(result).data def delete(self) -> None: @@ -142,7 +146,7 @@ async def update( is_enabled: bool | None = None, is_exclusive: bool | None = None, name: str | None = None, - actions: list[dict] | None = None, + actions: list[dict[str, Any]] | None = None, description: str | None = None, timezone: str | None = None, title: str | None = None, @@ -166,19 +170,17 @@ async def update( Returns: The updated schedule. """ - schedule_representation = get_schedule_repr( + request = ScheduleCreate( cron_expression=cron_expression, is_enabled=is_enabled, is_exclusive=is_exclusive, name=name, - actions=actions, + actions=[ScheduleCreateActions.model_validate(a) for a in actions] if actions else None, description=description, timezone=timezone, title=title, ) - cleaned = filter_none_values(schedule_representation) - - result = await self._update(cleaned) + result = await self._update(**request.model_dump(by_alias=True, exclude_none=True)) return ScheduleResponse.model_validate(result).data async def delete(self) -> None: diff --git a/src/apify_client/_resource_clients/schedule_collection.py b/src/apify_client/_resource_clients/schedule_collection.py index a335b213..29e0d43a 100644 --- a/src/apify_client/_resource_clients/schedule_collection.py +++ b/src/apify_client/_resource_clients/schedule_collection.py @@ -3,10 +3,15 @@ from typing import Any from apify_client._docs import docs_group -from apify_client._models import ListOfSchedules, ListOfSchedulesResponse, Schedule, ScheduleResponse -from apify_client._representations import get_schedule_repr +from apify_client._models import ( + ListOfSchedules, + ListOfSchedulesResponse, + Schedule, + ScheduleCreate, + ScheduleCreateActions, + ScheduleResponse, +) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._utils import filter_none_values @docs_group('Resource clients') @@ -50,7 +55,7 @@ def create( is_enabled: bool, is_exclusive: bool, name: str | None = None, - actions: list[dict] | None = None, # ty: ignore[invalid-type-form] + actions: list[dict[str, Any]] | None = None, # ty: ignore[invalid-type-form] description: str | None = None, timezone: str | None = None, title: str | None = None, @@ -77,18 +82,17 @@ def create( if not actions: actions = [] - schedule_representation = get_schedule_repr( + request = ScheduleCreate( cron_expression=cron_expression, is_enabled=is_enabled, is_exclusive=is_exclusive, name=name, - actions=actions, + actions=[ScheduleCreateActions.model_validate(a) for a in actions] if actions else None, description=description, timezone=timezone, title=title, ) - - result = self._create(filter_none_values(schedule_representation)) + result = self._create(**request.model_dump(by_alias=True, exclude_none=True)) return ScheduleResponse.model_validate(result).data @@ -133,7 +137,7 @@ async def create( is_enabled: bool, is_exclusive: bool, name: str | None = None, - actions: list[dict] | None = None, # ty: ignore[invalid-type-form] + actions: list[dict[str, Any]] | None = None, # ty: ignore[invalid-type-form] description: str | None = None, timezone: str | None = None, title: str | None = None, @@ -160,16 +164,15 @@ async def create( if not actions: actions = [] - schedule_representation = get_schedule_repr( + request = ScheduleCreate( cron_expression=cron_expression, is_enabled=is_enabled, is_exclusive=is_exclusive, name=name, - actions=actions, + actions=[ScheduleCreateActions.model_validate(a) for a in actions] if actions else None, description=description, timezone=timezone, title=title, ) - - result = await self._create(filter_none_values(schedule_representation)) + result = await self._create(**request.model_dump(by_alias=True, exclude_none=True)) return ScheduleResponse.model_validate(result).data diff --git a/src/apify_client/_resource_clients/task.py b/src/apify_client/_resource_clients/task.py index 04d76883..01e1f60f 100644 --- a/src/apify_client/_resource_clients/task.py +++ b/src/apify_client/_resource_clients/task.py @@ -3,14 +3,21 @@ from typing import TYPE_CHECKING, Any from apify_client._docs import docs_group -from apify_client._models import Run, RunOrigin, RunResponse, Task, TaskResponse -from apify_client._representations import get_task_repr +from apify_client._models import ( + ActorStandby, + Run, + RunOrigin, + RunResponse, + Task, + TaskInput, + TaskOptions, + TaskResponse, + UpdateTaskRequest, +) 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, - enum_to_value, - filter_none_values, response_to_dict, to_seconds, ) @@ -107,24 +114,26 @@ def update( Returns: The updated task. """ - task_representation = get_task_repr( + request = UpdateTaskRequest( name=name, - task_input=task_input, - build=build, - max_items=max_items, - memory_mbytes=memory_mbytes, - timeout=timeout, - restart_on_error=restart_on_error, title=title, - actor_standby_desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, - actor_standby_max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, - actor_standby_idle_timeout=actor_standby_idle_timeout, - actor_standby_build=actor_standby_build, - actor_standby_memory_mbytes=actor_standby_memory_mbytes, + input=TaskInput.model_validate(task_input) if task_input else None, + options=TaskOptions( + build=build, + max_items=max_items, + memory_mbytes=memory_mbytes, + timeout_secs=to_seconds(timeout, as_int=True), + restart_on_error=restart_on_error, + ), + actor_standby=ActorStandby( + desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, + max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, + idle_timeout_secs=to_seconds(actor_standby_idle_timeout, as_int=True), + build=actor_standby_build, + memory_mbytes=actor_standby_memory_mbytes, + ), ) - cleaned = filter_none_values(task_representation, remove_empty_dicts=True) - - result = self._update(cleaned) + result = self._update(**request.model_dump(by_alias=True, exclude_none=True)) return TaskResponse.model_validate(result).data def delete(self) -> None: @@ -314,7 +323,7 @@ def last_run(self, *, status: ActorJobStatus | None = None, origin: RunOrigin | return self._client_registry.run_client( resource_id='last', resource_path='runs', - params=self._build_params(status=enum_to_value(status), origin=enum_to_value(origin)), + params=self._build_params(status=status, origin=origin), **self._base_client_kwargs, ) @@ -400,24 +409,26 @@ async def update( Returns: The updated task. """ - task_representation = get_task_repr( + request = UpdateTaskRequest( name=name, - task_input=task_input, - build=build, - max_items=max_items, - memory_mbytes=memory_mbytes, - timeout=timeout, - restart_on_error=restart_on_error, title=title, - actor_standby_desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, - actor_standby_max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, - actor_standby_idle_timeout=actor_standby_idle_timeout, - actor_standby_build=actor_standby_build, - actor_standby_memory_mbytes=actor_standby_memory_mbytes, + input=TaskInput.model_validate(task_input) if task_input else None, + options=TaskOptions( + build=build, + max_items=max_items, + memory_mbytes=memory_mbytes, + timeout_secs=to_seconds(timeout, as_int=True), + restart_on_error=restart_on_error, + ), + actor_standby=ActorStandby( + desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, + max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, + idle_timeout_secs=to_seconds(actor_standby_idle_timeout, as_int=True), + build=actor_standby_build, + memory_mbytes=actor_standby_memory_mbytes, + ), ) - cleaned = filter_none_values(task_representation, remove_empty_dicts=True) - - result = await self._update(cleaned) + result = await self._update(**request.model_dump(by_alias=True, exclude_none=True)) return TaskResponse.model_validate(result).data async def delete(self) -> None: @@ -606,7 +617,7 @@ def last_run(self, *, status: ActorJobStatus | None = None, origin: RunOrigin | return self._client_registry.run_client( resource_id='last', resource_path='runs', - params=self._build_params(status=enum_to_value(status), origin=enum_to_value(origin)), + params=self._build_params(status=status, origin=origin), **self._base_client_kwargs, ) diff --git a/src/apify_client/_resource_clients/task_collection.py b/src/apify_client/_resource_clients/task_collection.py index b70bcae0..961753d9 100644 --- a/src/apify_client/_resource_clients/task_collection.py +++ b/src/apify_client/_resource_clients/task_collection.py @@ -3,10 +3,18 @@ from typing import TYPE_CHECKING, Any from apify_client._docs import docs_group -from apify_client._models import ListOfTasks, ListOfTasksResponse, Task, TaskResponse -from apify_client._representations import get_task_repr +from apify_client._models import ( + ActorStandby, + CreateTaskRequest, + ListOfTasks, + ListOfTasksResponse, + Task, + TaskInput, + TaskOptions, + TaskResponse, +) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._utils import filter_none_values +from apify_client._utils import to_seconds if TYPE_CHECKING: from datetime import timedelta @@ -95,24 +103,27 @@ def create( Returns: The created task. """ - task_representation = get_task_repr( - actor_id=actor_id, + request = CreateTaskRequest( + act_id=actor_id, name=name, - task_input=task_input, - build=build, - max_items=max_items, - memory_mbytes=memory_mbytes, - timeout=timeout, - restart_on_error=restart_on_error, title=title, - actor_standby_desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, - actor_standby_max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, - actor_standby_idle_timeout=actor_standby_idle_timeout, - actor_standby_build=actor_standby_build, - actor_standby_memory_mbytes=actor_standby_memory_mbytes, + input=TaskInput.model_validate(task_input) if task_input else None, + options=TaskOptions( + build=build, + max_items=max_items, + memory_mbytes=memory_mbytes, + timeout_secs=to_seconds(timeout, as_int=True), + restart_on_error=restart_on_error, + ), + actor_standby=ActorStandby( + desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, + max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, + idle_timeout_secs=to_seconds(actor_standby_idle_timeout, as_int=True), + build=actor_standby_build, + memory_mbytes=actor_standby_memory_mbytes, + ), ) - - result = self._create(filter_none_values(task_representation, remove_empty_dicts=True)) + result = self._create(**request.model_dump(by_alias=True, exclude_none=True)) return TaskResponse.model_validate(result).data @@ -199,22 +210,25 @@ async def create( Returns: The created task. """ - task_representation = get_task_repr( - actor_id=actor_id, + request = CreateTaskRequest( + act_id=actor_id, name=name, - task_input=task_input, - build=build, - max_items=max_items, - memory_mbytes=memory_mbytes, - timeout=timeout, - restart_on_error=restart_on_error, title=title, - actor_standby_desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, - actor_standby_max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, - actor_standby_idle_timeout=actor_standby_idle_timeout, - actor_standby_build=actor_standby_build, - actor_standby_memory_mbytes=actor_standby_memory_mbytes, + input=TaskInput.model_validate(task_input) if task_input else None, + options=TaskOptions( + build=build, + max_items=max_items, + memory_mbytes=memory_mbytes, + timeout_secs=to_seconds(timeout, as_int=True), + restart_on_error=restart_on_error, + ), + actor_standby=ActorStandby( + desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run, + max_requests_per_actor_run=actor_standby_max_requests_per_actor_run, + idle_timeout_secs=to_seconds(actor_standby_idle_timeout, as_int=True), + build=actor_standby_build, + memory_mbytes=actor_standby_memory_mbytes, + ), ) - - result = await self._create(filter_none_values(task_representation, remove_empty_dicts=True)) + result = await self._create(**request.model_dump(by_alias=True, exclude_none=True)) return TaskResponse.model_validate(result).data diff --git a/src/apify_client/_resource_clients/user.py b/src/apify_client/_resource_clients/user.py index 165242f8..a30a5304 100644 --- a/src/apify_client/_resource_clients/user.py +++ b/src/apify_client/_resource_clients/user.py @@ -16,7 +16,7 @@ UserPublicInfo, ) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._utils import catch_not_found_or_throw, filter_none_values, response_to_dict +from apify_client._utils import catch_not_found_or_throw, response_to_dict from apify_client.errors import ApifyApiError @@ -115,7 +115,7 @@ def update_limits( url=self._build_url('limits'), method='PUT', params=self._build_params(), - json=filter_none_values( + json=self._clean_json_payload( { 'maxMonthlyUsageUsd': max_monthly_usage_usd, 'dataRetentionDays': data_retention_days, @@ -219,7 +219,7 @@ async def update_limits( url=self._build_url('limits'), method='PUT', params=self._build_params(), - json=filter_none_values( + json=self._clean_json_payload( { 'maxMonthlyUsageUsd': max_monthly_usage_usd, 'dataRetentionDays': data_retention_days, diff --git a/src/apify_client/_resource_clients/webhook.py b/src/apify_client/_resource_clients/webhook.py index bb70e23f..2ba692e1 100644 --- a/src/apify_client/_resource_clients/webhook.py +++ b/src/apify_client/_resource_clients/webhook.py @@ -2,16 +2,19 @@ from typing import TYPE_CHECKING, Any +from pydantic import AnyUrl + from apify_client._docs import docs_group from apify_client._models import ( TestWebhookResponse, Webhook, + WebhookCondition, WebhookDispatch, WebhookResponse, + WebhookUpdate, ) -from apify_client._representations import get_webhook_repr from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._utils import catch_not_found_or_throw, filter_none_values, response_to_dict +from apify_client._utils import catch_not_found_or_throw, response_to_dict from apify_client.errors import ApifyApiError if TYPE_CHECKING: @@ -78,21 +81,24 @@ def update( Returns: The updated webhook. """ - webhook_representation = get_webhook_repr( - event_types=event_types, - request_url=request_url, + if actor_run_id is not None and is_ad_hoc is None: + is_ad_hoc = True + + request = WebhookUpdate( + event_types=list(event_types) if event_types is not None else None, + request_url=AnyUrl(request_url) if request_url is not None else None, payload_template=payload_template, headers_template=headers_template, - actor_id=actor_id, - actor_task_id=actor_task_id, - actor_run_id=actor_run_id, ignore_ssl_errors=ignore_ssl_errors, do_not_retry=do_not_retry, is_ad_hoc=is_ad_hoc, + condition=WebhookCondition( + actor_run_id=actor_run_id, + actor_task_id=actor_task_id, + actor_id=actor_id, + ), ) - cleaned = filter_none_values(webhook_representation, remove_empty_dicts=True) - - result = self._update(cleaned) + result = self._update(**request.model_dump(by_alias=True, exclude_none=True)) return WebhookResponse.model_validate(result).data def delete(self) -> None: @@ -200,21 +206,24 @@ async def update( Returns: The updated webhook. """ - webhook_representation = get_webhook_repr( - event_types=event_types, - request_url=request_url, + if actor_run_id is not None and is_ad_hoc is None: + is_ad_hoc = True + + request = WebhookUpdate( + event_types=list(event_types) if event_types is not None else None, + request_url=AnyUrl(request_url) if request_url is not None else None, payload_template=payload_template, headers_template=headers_template, - actor_id=actor_id, - actor_task_id=actor_task_id, - actor_run_id=actor_run_id, ignore_ssl_errors=ignore_ssl_errors, do_not_retry=do_not_retry, is_ad_hoc=is_ad_hoc, + condition=WebhookCondition( + actor_run_id=actor_run_id, + actor_task_id=actor_task_id, + actor_id=actor_id, + ), ) - cleaned = filter_none_values(webhook_representation, remove_empty_dicts=True) - - result = await self._update(cleaned) + result = await self._update(**request.model_dump(by_alias=True, exclude_none=True)) return WebhookResponse.model_validate(result).data async def delete(self) -> None: diff --git a/src/apify_client/_resource_clients/webhook_collection.py b/src/apify_client/_resource_clients/webhook_collection.py index db4de310..edf90646 100644 --- a/src/apify_client/_resource_clients/webhook_collection.py +++ b/src/apify_client/_resource_clients/webhook_collection.py @@ -2,14 +2,20 @@ from typing import TYPE_CHECKING, Any +from pydantic import AnyUrl + from apify_client._docs import docs_group -from apify_client._models import ListOfWebhooks, ListOfWebhooksResponse, Webhook, WebhookResponse -from apify_client._representations import get_webhook_repr +from apify_client._models import ( + ListOfWebhooks, + ListOfWebhooksResponse, + WebhookCondition, + WebhookCreate, + WebhookResponse, +) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync -from apify_client._utils import filter_none_values if TYPE_CHECKING: - from apify_client._models import WebhookEventType + from apify_client._models import Webhook, WebhookEventType @docs_group('Resource clients') @@ -85,21 +91,25 @@ def create( Returns: The created webhook. """ - webhook_representation = get_webhook_repr( - event_types=event_types, - request_url=request_url, + if actor_run_id is not None and is_ad_hoc is None: + is_ad_hoc = True + + request = WebhookCreate( + event_types=list(event_types), + request_url=AnyUrl(request_url), payload_template=payload_template, headers_template=headers_template, - actor_id=actor_id, - actor_task_id=actor_task_id, - actor_run_id=actor_run_id, ignore_ssl_errors=ignore_ssl_errors, do_not_retry=do_not_retry, idempotency_key=idempotency_key, is_ad_hoc=is_ad_hoc, + condition=WebhookCondition( + actor_run_id=actor_run_id, + actor_task_id=actor_task_id, + actor_id=actor_id, + ), ) - - result = self._create(filter_none_values(webhook_representation, remove_empty_dicts=True)) + result = self._create(**request.model_dump(by_alias=True, exclude_none=True)) return WebhookResponse.model_validate(result).data @@ -176,19 +186,23 @@ async def create( Returns: The created webhook. """ - webhook_representation = get_webhook_repr( - event_types=event_types, - request_url=request_url, + if actor_run_id is not None and is_ad_hoc is None: + is_ad_hoc = True + + request = WebhookCreate( + event_types=list(event_types), + request_url=AnyUrl(request_url), payload_template=payload_template, headers_template=headers_template, - actor_id=actor_id, - actor_task_id=actor_task_id, - actor_run_id=actor_run_id, ignore_ssl_errors=ignore_ssl_errors, do_not_retry=do_not_retry, idempotency_key=idempotency_key, is_ad_hoc=is_ad_hoc, + condition=WebhookCondition( + actor_run_id=actor_run_id, + actor_task_id=actor_task_id, + actor_id=actor_id, + ), ) - - result = await self._create(filter_none_values(webhook_representation, remove_empty_dicts=True)) + result = await self._create(**request.model_dump(by_alias=True, exclude_none=True)) return WebhookResponse.model_validate(result).data diff --git a/src/apify_client/_utils.py b/src/apify_client/_utils.py index 00eb297e..909411eb 100644 --- a/src/apify_client/_utils.py +++ b/src/apify_client/_utils.py @@ -7,7 +7,6 @@ import string import time from base64 import b64encode, urlsafe_b64encode -from enum import Enum from http import HTTPStatus from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload @@ -69,69 +68,6 @@ def catch_not_found_or_throw(exc: ApifyApiError) -> None: raise exc -def filter_none_values( - data: dict, - *, - remove_empty_dicts: bool | None = None, -) -> dict: - """Recursively remove None values from a dictionary. - - The Apify API ignores missing fields but may reject fields explicitly set to None. This helper prepares - request payloads by stripping None values from nested dictionaries. - - Uses an iterative, stack-based approach for better performance on deeply nested structures. - - Args: - data: Dictionary to clean. - remove_empty_dicts: Whether to remove empty dictionaries after filtering. - - Returns: - A new dictionary with all None values removed. - """ - # Use an explicit stack to avoid recursion overhead - result = {} - - # Stack entries are (source_dict, target_dict) - stack: list[tuple[dict, dict]] = [(data, result)] - - while stack: - source, target = stack.pop() - - for key, val in source.items(): - if val is None: - continue - - if isinstance(val, dict): - nested = {} - target[key] = nested - stack.append((val, nested)) - else: - target[key] = val - - # Optionally remove empty dictionaries - if remove_empty_dicts: - _remove_empty_dicts_inplace(result) - - return result - - -def _remove_empty_dicts_inplace(data: dict[str, Any]) -> None: - """Recursively remove empty dictionaries from a dict in place. - - This is a helper function for filter_none_values. - """ - keys_to_remove = list[str]() - - for key, val in data.items(): - if isinstance(val, dict): - _remove_empty_dicts_inplace(val) - if not val: - keys_to_remove.append(key) - - for key in keys_to_remove: - del data[key] - - def encode_webhook_list_to_base64(webhooks: list[dict]) -> str: """Encode a list of webhook dictionaries to base64 for API transmission. @@ -145,7 +81,7 @@ def encode_webhook_list_to_base64(webhooks: list[dict]) -> str: for webhook in webhooks: webhook_representation = { - 'eventTypes': [enum_to_value(event_type) for event_type in webhook['event_types']], + 'eventTypes': list(webhook['event_types']), 'requestUrl': webhook['request_url'], } if 'payload_template' in webhook: @@ -191,22 +127,6 @@ def encode_key_value_store_record_value(value: Any, content_type: str | None = N return (value, content_type) -def enum_to_value(value: Any) -> Any: - """Convert an Enum member to its value, or return the value unchanged if not an Enum. - - Ensures Enum instances are converted to primitive values suitable for API transmission. - - Args: - value: The value to potentially convert (Enum member or any other type). - - Returns: - The Enum's value if the input is an Enum; otherwise returns the input unchanged. - """ - if isinstance(value, Enum): - return value.value - return value - - def is_retryable_error(exc: Exception) -> bool: """Check if the given error is retryable. diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 4478b425..bdaef624 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,6 +1,5 @@ import io from datetime import timedelta -from enum import Enum from http import HTTPStatus from unittest.mock import Mock @@ -8,6 +7,7 @@ import pytest from apify_client._models import WebhookEventType +from apify_client._resource_clients._resource_client import ResourceClientBase from apify_client._utils import ( catch_not_found_or_throw, create_hmac_signature, @@ -15,8 +15,6 @@ encode_base62, encode_key_value_store_record_value, encode_webhook_list_to_base64, - enum_to_value, - filter_none_values, is_retryable_error, response_to_dict, response_to_list, @@ -108,22 +106,19 @@ def test_catch_not_found_or_throw(status_code: HTTPStatus, error_type: str, *, s @pytest.mark.parametrize( - ('input_dict', 'remove_empty_dicts', 'expected'), + ('input_dict', 'expected'), [ - pytest.param({'a': 1, 'b': None, 'c': 3}, False, {'a': 1, 'c': 3}, id='Simple case'), - pytest.param({'a': {'b': None, 'c': 2}, 'd': None}, False, {'a': {'c': 2}}, id='Nested dictionaries'), - pytest.param({'a': {'b': {'c': None, 'd': 4}}}, False, {'a': {'b': {'d': 4}}}, id='Deep nesting'), - pytest.param({'a': None, 'b': None}, False, {}, id='Empty dict after filtering'), - pytest.param({'a': {'b': None}, 'c': 3}, True, {'c': 3}, id='Remove empty dicts'), - pytest.param({'a': {'b': None}, 'c': 3}, False, {'a': {}, 'c': 3}, id='Keep empty dicts by default'), - pytest.param( - {'a': 0, 'b': '', 'c': False}, False, {'a': 0, 'b': '', 'c': False}, id='Keep falsy non-None values' - ), + pytest.param({'a': 1, 'b': None, 'c': 3}, {'a': 1, 'c': 3}, id='Simple case'), + pytest.param({'a': {'b': None, 'c': 2}, 'd': None}, {'a': {'c': 2}}, id='Nested dictionaries'), + pytest.param({'a': {'b': {'c': None, 'd': 4}}}, {'a': {'b': {'d': 4}}}, id='Deep nesting'), + pytest.param({'a': None, 'b': None}, {}, id='Empty dict after filtering'), + pytest.param({'a': {'b': None}, 'c': 3}, {'c': 3}, id='Remove empty dicts'), + pytest.param({'a': 0, 'b': '', 'c': False}, {'a': 0, 'b': '', 'c': False}, id='Keep falsy non-None values'), ], ) -def test_filter_none_values(input_dict: dict, *, remove_empty_dicts: bool, expected: dict) -> None: - """Test filtering None values from dictionaries.""" - assert filter_none_values(input_dict, remove_empty_dicts=remove_empty_dicts) == expected +def test__clean_json_payload(input_dict: dict, expected: dict) -> None: + """Test cleaning None values and empty dicts from API request payloads.""" + assert ResourceClientBase._clean_json_payload(input_dict) == expected def test_encode_key_value_store_record_value_dict() -> None: @@ -162,26 +157,6 @@ def test_encode_key_value_store_record_value_bytesio() -> None: assert content_type == 'application/octet-stream' -class _TestEnum(Enum): - VALUE1 = 'val1' - VALUE2 = 42 - - -@pytest.mark.parametrize( - ('input_value', 'expected'), - [ - pytest.param(_TestEnum.VALUE1, 'val1', id='Enum string value'), - pytest.param(_TestEnum.VALUE2, 42, id='Enum int value'), - pytest.param('not_an_enum', 'not_an_enum', id='Plain string passthrough'), - pytest.param(123, 123, id='Plain int passthrough'), - pytest.param(None, None, id='None passthrough'), - ], -) -def test_enum_to_value(input_value: _TestEnum | str | int | None, expected: str | int | None) -> None: - """Test enum to value conversion.""" - assert enum_to_value(input_value) == expected - - def test_response_to_dict() -> None: """Test parsing response as dictionary.""" mock_response = Mock() From 1b03bbc872176343b16a104ac61140e03fb8b7d7 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Wed, 25 Feb 2026 17:23:12 +0100 Subject: [PATCH 2/2] fix: Restore timeout parameter on _update() method Co-Authored-By: Claude Opus 4.6 --- src/apify_client/_resource_clients/_resource_client.py | 6 ++++-- src/apify_client/_resource_clients/dataset.py | 4 ++-- src/apify_client/_resource_clients/key_value_store.py | 4 ++-- src/apify_client/_resource_clients/request_queue.py | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/apify_client/_resource_clients/_resource_client.py b/src/apify_client/_resource_clients/_resource_client.py index d38978de..a2847499 100644 --- a/src/apify_client/_resource_clients/_resource_client.py +++ b/src/apify_client/_resource_clients/_resource_client.py @@ -206,13 +206,14 @@ def _get(self, *, timeout: timedelta | None = None) -> dict | None: catch_not_found_or_throw(exc) return None - def _update(self, **kwargs: Any) -> dict: + def _update(self, *, timeout: timedelta | None = None, **kwargs: Any) -> dict: """Perform a PUT request to update this resource with the given fields.""" response = self._http_client.call( url=self._build_url(), method='PUT', params=self._build_params(), json=self._clean_json_payload(kwargs), + timeout=timeout, ) return response_to_dict(response) @@ -372,13 +373,14 @@ async def _get(self, *, timeout: timedelta | None = None) -> dict | None: catch_not_found_or_throw(exc) return None - async def _update(self, **kwargs: Any) -> dict: + async def _update(self, *, timeout: timedelta | None = None, **kwargs: Any) -> dict: """Perform a PUT request to update this resource with the given fields.""" response = await self._http_client.call( url=self._build_url(), method='PUT', params=self._build_params(), json=self._clean_json_payload(kwargs), + timeout=timeout, ) return response_to_dict(response) diff --git a/src/apify_client/_resource_clients/dataset.py b/src/apify_client/_resource_clients/dataset.py index 975b15e8..e5be1d68 100644 --- a/src/apify_client/_resource_clients/dataset.py +++ b/src/apify_client/_resource_clients/dataset.py @@ -94,7 +94,7 @@ def update(self, *, name: str | None = None, general_access: GeneralAccess | Non Returns: The updated dataset. """ - result = self._update(name=name, generalAccess=general_access) + result = self._update(timeout=FAST_OPERATION_TIMEOUT, name=name, generalAccess=general_access) return DatasetResponse.model_validate(result).data def delete(self) -> None: @@ -709,7 +709,7 @@ async def update(self, *, name: str | None = None, general_access: GeneralAccess Returns: The updated dataset. """ - result = await self._update(name=name, generalAccess=general_access) + result = await self._update(timeout=FAST_OPERATION_TIMEOUT, name=name, generalAccess=general_access) return DatasetResponse.model_validate(result).data async def delete(self) -> None: diff --git a/src/apify_client/_resource_clients/key_value_store.py b/src/apify_client/_resource_clients/key_value_store.py index af3be937..bd0a637e 100644 --- a/src/apify_client/_resource_clients/key_value_store.py +++ b/src/apify_client/_resource_clients/key_value_store.py @@ -105,7 +105,7 @@ def update(self, *, name: str | None = None, general_access: GeneralAccess | Non Returns: The updated key-value store. """ - result = self._update(name=name, generalAccess=general_access) + result = self._update(timeout=FAST_OPERATION_TIMEOUT, name=name, generalAccess=general_access) return KeyValueStoreResponse.model_validate(result).data def delete(self) -> None: @@ -488,7 +488,7 @@ async def update( Returns: The updated key-value store. """ - result = await self._update(name=name, generalAccess=general_access) + result = await self._update(timeout=FAST_OPERATION_TIMEOUT, name=name, generalAccess=general_access) return KeyValueStoreResponse.model_validate(result).data async def delete(self) -> None: diff --git a/src/apify_client/_resource_clients/request_queue.py b/src/apify_client/_resource_clients/request_queue.py index 0fe613fb..3ae18519 100644 --- a/src/apify_client/_resource_clients/request_queue.py +++ b/src/apify_client/_resource_clients/request_queue.py @@ -100,7 +100,7 @@ def update(self, *, name: str | None = None, general_access: GeneralAccess | Non Returns: The updated request queue. """ - result = self._update(name=name, generalAccess=general_access) + result = self._update(timeout=FAST_OPERATION_TIMEOUT, name=name, generalAccess=general_access) return RequestQueueResponse.model_validate(result).data def delete(self) -> None: @@ -509,7 +509,7 @@ async def update( Returns: The updated request queue. """ - result = await self._update(name=name, generalAccess=general_access) + result = await self._update(timeout=FAST_OPERATION_TIMEOUT, name=name, generalAccess=general_access) return RequestQueueResponse.model_validate(result).data async def delete(self) -> None: