diff --git a/src/apify_client/_resource_clients/actor_collection.py b/src/apify_client/_resource_clients/actor_collection.py index e7241e0c..3d20423b 100644 --- a/src/apify_client/_resource_clients/actor_collection.py +++ b/src/apify_client/_resource_clients/actor_collection.py @@ -137,6 +137,7 @@ def create( actor_standby_memory_mbytes: int | None = None, actor_standby_disable_standby_fields_override: bool | None = None, actor_standby_should_pass_actor_input: bool | None = None, + actor_permission_level: ActorPermissionLevel | None = None, timeout: Timeout = 'medium', ) -> ActorResource: """Create a new Actor. @@ -178,6 +179,7 @@ def create( overridden elsewhere. actor_standby_should_pass_actor_input: Whether to pass the Actor input to the Standby runs. If false, the Standby runs start with no input. + actor_permission_level: The permission level of the Actor on Apify platform. timeout: Timeout for the API HTTP request. Returns: @@ -191,6 +193,7 @@ def create( seo_description=seo_description, versions=versions, is_public=is_public, + actor_permission_level=actor_permission_level, is_deprecated=is_deprecated, categories=categories, default_run_options=DefaultRunOptions( @@ -331,6 +334,7 @@ async def create( actor_standby_memory_mbytes: int | None = None, actor_standby_disable_standby_fields_override: bool | None = None, actor_standby_should_pass_actor_input: bool | None = None, + actor_permission_level: ActorPermissionLevel | None = None, timeout: Timeout = 'medium', ) -> ActorResource: """Create a new Actor. @@ -372,6 +376,7 @@ async def create( overridden elsewhere. actor_standby_should_pass_actor_input: Whether to pass the Actor input to the Standby runs. If false, the Standby runs start with no input. + actor_permission_level: The permission level of the Actor on Apify platform. timeout: Timeout for the API HTTP request. Returns: @@ -385,6 +390,7 @@ async def create( seo_description=seo_description, versions=versions, is_public=is_public, + actor_permission_level=actor_permission_level, is_deprecated=is_deprecated, categories=categories, default_run_options=DefaultRunOptions( diff --git a/src/apify_client/_resource_clients/dataset.py b/src/apify_client/_resource_clients/dataset.py index eecea162..7df6cccd 100644 --- a/src/apify_client/_resource_clients/dataset.py +++ b/src/apify_client/_resource_clients/dataset.py @@ -90,6 +90,7 @@ def update( self, *, name: str | None = None, + title: str | None = None, general_access: GeneralAccess | None = None, timeout: Timeout = 'short', ) -> DatasetResource: @@ -99,6 +100,7 @@ def update( Args: name: The new name for the dataset. + title: The new title for the dataset. general_access: Determines how others can access the dataset. timeout: Timeout for the API HTTP request. @@ -108,6 +110,7 @@ def update( result = self._update( timeout=timeout, name=name, + title=title, generalAccess=general_access, ) return DatasetResponse.model_validate(result).data @@ -224,6 +227,8 @@ def iterate_items( unwind: list[str] | None = None, skip_empty: bool | None = None, skip_hidden: bool | None = None, + flatten: list[str] | None = None, + view: str | None = None, signature: str | None = None, chunk_size: int | None = None, timeout: Timeout = 'long', @@ -260,6 +265,8 @@ def iterate_items( contain less items than the limit value. skip_hidden: If True, then hidden fields are skipped from the output, i.e. fields starting with the # character. + flatten: A list of fields that should be flattened. + view: Name of the dataset view to be used. signature: Signature used to access the items. chunk_size: Maximum number of dataset rows requested per API call when iterating across pages. timeout: Timeout for the API HTTP request. @@ -279,6 +286,8 @@ def _callback(*, limit: int | None = None, offset: int | None = None) -> Dataset unwind=unwind, skip_empty=skip_empty, skip_hidden=skip_hidden, + flatten=flatten, + view=view, signature=signature, timeout=timeout, ) @@ -304,6 +313,8 @@ def get_items_as_bytes( xml_root: str | None = None, xml_row: str | None = None, flatten: list[str] | None = None, + view: str | None = None, + attachment: bool | None = None, signature: str | None = None, timeout: Timeout = 'long', ) -> bytes: @@ -348,6 +359,9 @@ def get_items_as_bytes( xml_row: Overrides default element name that wraps each page or page function result object in xml output. By default the element name is item. flatten: A list of fields that should be flattened. + view: Name of the dataset view to be used. + attachment: If True, the response defines the `Content-Disposition: attachment` header, which makes a web + browser download the file without displaying it. signature: Signature used to access the items. timeout: Timeout for the API HTTP request. @@ -371,6 +385,8 @@ def get_items_as_bytes( xmlRoot=xml_root, xmlRow=xml_row, flatten=flatten, + view=view, + attachment=attachment, signature=signature, ) @@ -403,6 +419,8 @@ def stream_items( xml_root: str | None = None, xml_row: str | None = None, flatten: list[str] | None = None, + view: str | None = None, + attachment: bool | None = None, signature: str | None = None, timeout: Timeout = 'long', ) -> Generator[HttpResponse]: @@ -447,6 +465,9 @@ def stream_items( xml_row: Overrides default element name that wraps each page or page function result object in xml output. By default the element name is item. flatten: A list of fields that should be flattened. + view: Name of the dataset view to be used. + attachment: If True, the response defines the `Content-Disposition: attachment` header, which makes a web + browser download the file without displaying it. signature: Signature used to access the items. timeout: Timeout for the API HTTP request. @@ -472,6 +493,8 @@ def stream_items( xmlRoot=xml_root, xmlRow=xml_row, flatten=flatten, + view=view, + attachment=attachment, signature=signature, ) @@ -541,6 +564,7 @@ def get_statistics(self, *, timeout: Timeout = 'short') -> DatasetStatistics: def create_items_public_url( self, *, + item_format: str | None = None, offset: int | None = None, limit: int | None = None, clean: bool | None = None, @@ -567,6 +591,8 @@ def create_items_public_url( Any other options (like `limit` or `offset`) will be included as query parameters in the URL. Args: + item_format: Format of the results, possible values are: json, jsonl, csv, html, xlsx, xml and rss. + By default, the URL serves JSON. offset: Number of items that should be skipped at the start. The default value is 0. limit: Maximum number of items to return. By default there is no limit. clean: If True, returns only non-empty items and skips hidden fields. @@ -588,6 +614,7 @@ def create_items_public_url( dataset = self.get(timeout=timeout) request_params = self._build_params( + format=item_format, offset=offset, limit=limit, desc=desc, @@ -653,6 +680,7 @@ async def update( self, *, name: str | None = None, + title: str | None = None, general_access: GeneralAccess | None = None, timeout: Timeout = 'short', ) -> DatasetResource: @@ -662,6 +690,7 @@ async def update( Args: name: The new name for the dataset. + title: The new title for the dataset. general_access: Determines how others can access the dataset. timeout: Timeout for the API HTTP request. @@ -671,6 +700,7 @@ async def update( result = await self._update( timeout=timeout, name=name, + title=title, generalAccess=general_access, ) return DatasetResponse.model_validate(result).data @@ -787,6 +817,8 @@ def iterate_items( unwind: list[str] | None = None, skip_empty: bool | None = None, skip_hidden: bool | None = None, + flatten: list[str] | None = None, + view: str | None = None, signature: str | None = None, chunk_size: int | None = None, timeout: Timeout = 'long', @@ -823,6 +855,8 @@ def iterate_items( contain less items than the limit value. skip_hidden: If True, then hidden fields are skipped from the output, i.e. fields starting with the # character. + flatten: A list of fields that should be flattened. + view: Name of the dataset view to be used. signature: Signature used to access the items. chunk_size: Maximum number of dataset rows requested per API call when iterating across pages. timeout: Timeout for the API HTTP request. @@ -842,6 +876,8 @@ async def _callback(*, limit: int | None = None, offset: int | None = None) -> D unwind=unwind, skip_empty=skip_empty, skip_hidden=skip_hidden, + flatten=flatten, + view=view, signature=signature, timeout=timeout, ) @@ -869,6 +905,8 @@ async def get_items_as_bytes( xml_root: str | None = None, xml_row: str | None = None, flatten: list[str] | None = None, + view: str | None = None, + attachment: bool | None = None, signature: str | None = None, timeout: Timeout = 'long', ) -> bytes: @@ -913,6 +951,9 @@ async def get_items_as_bytes( xml_row: Overrides default element name that wraps each page or page function result object in xml output. By default the element name is item. flatten: A list of fields that should be flattened. + view: Name of the dataset view to be used. + attachment: If True, the response defines the `Content-Disposition: attachment` header, which makes a web + browser download the file without displaying it. signature: Signature used to access the items. timeout: Timeout for the API HTTP request. @@ -936,6 +977,8 @@ async def get_items_as_bytes( xmlRoot=xml_root, xmlRow=xml_row, flatten=flatten, + view=view, + attachment=attachment, signature=signature, ) @@ -968,6 +1011,8 @@ async def stream_items( xml_root: str | None = None, xml_row: str | None = None, flatten: list[str] | None = None, + view: str | None = None, + attachment: bool | None = None, signature: str | None = None, timeout: Timeout = 'long', ) -> AsyncGenerator[HttpResponse]: @@ -1012,6 +1057,9 @@ async def stream_items( xml_row: Overrides default element name that wraps each page or page function result object in xml output. By default the element name is item. flatten: A list of fields that should be flattened. + view: Name of the dataset view to be used. + attachment: If True, the response defines the `Content-Disposition: attachment` header, which makes a web + browser download the file without displaying it. signature: Signature used to access the items. timeout: Timeout for the API HTTP request. @@ -1037,6 +1085,8 @@ async def stream_items( xmlRoot=xml_root, xmlRow=xml_row, flatten=flatten, + view=view, + attachment=attachment, signature=signature, ) @@ -1106,6 +1156,7 @@ async def get_statistics(self, *, timeout: Timeout = 'short') -> DatasetStatisti async def create_items_public_url( self, *, + item_format: str | None = None, offset: int | None = None, limit: int | None = None, clean: bool | None = None, @@ -1132,6 +1183,8 @@ async def create_items_public_url( Any other options (like `limit` or `offset`) will be included as query parameters in the URL. Args: + item_format: Format of the results, possible values are: json, jsonl, csv, html, xlsx, xml and rss. + By default, the URL serves JSON. offset: Number of items that should be skipped at the start. The default value is 0. limit: Maximum number of items to return. By default there is no limit. clean: If True, returns only non-empty items and skips hidden fields. @@ -1153,6 +1206,7 @@ async def create_items_public_url( dataset = await self.get(timeout=timeout) request_params = self._build_params( + format=item_format, offset=offset, limit=limit, desc=desc, diff --git a/src/apify_client/_resource_clients/key_value_store.py b/src/apify_client/_resource_clients/key_value_store.py index f1df3f1d..aa8fe41b 100644 --- a/src/apify_client/_resource_clients/key_value_store.py +++ b/src/apify_client/_resource_clients/key_value_store.py @@ -105,6 +105,7 @@ def update( self, *, name: str | None = None, + title: str | None = None, general_access: GeneralAccess | None = None, timeout: Timeout = 'long', ) -> KeyValueStoreResource: @@ -114,13 +115,14 @@ def update( Args: name: The new name for key-value store. + title: The new title for the key-value store. general_access: Determines how others can access the key-value store. timeout: Timeout for the API HTTP request. Returns: The updated key-value store. """ - result = self._update(timeout=timeout, name=name, generalAccess=general_access) + result = self._update(timeout=timeout, name=name, title=title, generalAccess=general_access) return KeyValueStoreResponse.model_validate(result).data def delete(self, *, timeout: Timeout = 'short') -> None: @@ -541,6 +543,7 @@ async def update( self, *, name: str | None = None, + title: str | None = None, general_access: GeneralAccess | None = None, timeout: Timeout = 'long', ) -> KeyValueStoreResource: @@ -550,13 +553,14 @@ async def update( Args: name: The new name for key-value store. + title: The new title for the key-value store. general_access: Determines how others can access the key-value store. timeout: Timeout for the API HTTP request. Returns: The updated key-value store. """ - result = await self._update(timeout=timeout, name=name, generalAccess=general_access) + result = await self._update(timeout=timeout, name=name, title=title, generalAccess=general_access) return KeyValueStoreResponse.model_validate(result).data async def delete(self, *, timeout: Timeout = 'short') -> None: diff --git a/src/apify_client/_resource_clients/request_queue.py b/src/apify_client/_resource_clients/request_queue.py index 6f6834f9..0540bb35 100644 --- a/src/apify_client/_resource_clients/request_queue.py +++ b/src/apify_client/_resource_clients/request_queue.py @@ -140,6 +140,7 @@ def update( self, *, name: str | None = None, + title: str | None = None, general_access: GeneralAccess | None = None, timeout: Timeout = 'short', ) -> RequestQueueResource: @@ -149,13 +150,14 @@ def update( Args: name: The new name for the request queue. + title: The new title for the request queue. general_access: Determines how others can access the request queue. timeout: Timeout for the API HTTP request. Returns: The updated request queue. """ - result = self._update(timeout=timeout, name=name, generalAccess=general_access) + result = self._update(timeout=timeout, name=name, title=title, generalAccess=general_access) return RequestQueueResponse.model_validate(result).data def delete(self, *, timeout: Timeout = 'short') -> None: @@ -673,6 +675,7 @@ async def update( self, *, name: str | None = None, + title: str | None = None, general_access: GeneralAccess | None = None, timeout: Timeout = 'short', ) -> RequestQueueResource: @@ -682,13 +685,14 @@ async def update( Args: name: The new name for the request queue. + title: The new title for the request queue. general_access: Determines how others can access the request queue. timeout: Timeout for the API HTTP request. Returns: The updated request queue. """ - result = await self._update(timeout=timeout, name=name, generalAccess=general_access) + result = await self._update(timeout=timeout, name=name, title=title, generalAccess=general_access) return RequestQueueResponse.model_validate(result).data async def delete(self, *, timeout: Timeout = 'short') -> None: diff --git a/src/apify_client/_resource_clients/schedule.py b/src/apify_client/_resource_clients/schedule.py index cf5a9de6..2787e107 100644 --- a/src/apify_client/_resource_clients/schedule.py +++ b/src/apify_client/_resource_clients/schedule.py @@ -8,6 +8,7 @@ Schedule, ScheduleLogEntry, ScheduleLogResponse, + ScheduleNotifications, ScheduleResponse, ) from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync @@ -65,6 +66,7 @@ def update( description: str | None = None, timezone: str | None = None, title: str | None = None, + notifications_email: bool | None = None, timeout: Timeout = 'short', ) -> Schedule: """Update the schedule with specified fields. @@ -82,6 +84,7 @@ def update( timezone: Timezone in which your cron expression runs (TZ database name from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). title: A human-friendly equivalent of the name. + notifications_email: Whether to send email notifications about the schedule's runs. timeout: Timeout for the API HTTP request. Returns: @@ -96,6 +99,7 @@ def update( description=description, timezone=timezone, title=title, + notifications=ScheduleNotifications(email=notifications_email) if notifications_email is not None else None, ) result = self._update(timeout=timeout, **schedule_fields.model_dump(by_alias=True, exclude_none=True)) return ScheduleResponse.model_validate(result).data @@ -182,6 +186,7 @@ async def update( description: str | None = None, timezone: str | None = None, title: str | None = None, + notifications_email: bool | None = None, timeout: Timeout = 'short', ) -> Schedule: """Update the schedule with specified fields. @@ -199,6 +204,7 @@ async def update( timezone: Timezone in which your cron expression runs (TZ database name from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). title: A human-friendly equivalent of the name. + notifications_email: Whether to send email notifications about the schedule's runs. timeout: Timeout for the API HTTP request. Returns: @@ -213,6 +219,7 @@ async def update( description=description, timezone=timezone, title=title, + notifications=ScheduleNotifications(email=notifications_email) if notifications_email is not None else None, ) result = await self._update(timeout=timeout, **schedule_fields.model_dump(by_alias=True, exclude_none=True)) return ScheduleResponse.model_validate(result).data diff --git a/src/apify_client/_resource_clients/schedule_collection.py b/src/apify_client/_resource_clients/schedule_collection.py index 7e7afbc8..547383d8 100644 --- a/src/apify_client/_resource_clients/schedule_collection.py +++ b/src/apify_client/_resource_clients/schedule_collection.py @@ -8,6 +8,7 @@ ListOfSchedules, ListOfSchedulesResponse, Schedule, + ScheduleNotifications, ScheduleResponse, ) from apify_client._pagination import get_items_iterator, get_items_iterator_async @@ -106,6 +107,7 @@ def create( description: str | None = None, timezone: str | None = None, title: str | None = None, + notifications_email: bool | None = None, timeout: Timeout = 'short', ) -> Schedule: """Create a new schedule. @@ -123,6 +125,7 @@ def create( timezone: Timezone in which your cron expression runs (TZ database name from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). title: Title of this schedule. + notifications_email: Whether to send email notifications about the schedule's runs. timeout: Timeout for the API HTTP request. Returns: @@ -140,6 +143,7 @@ def create( description=description, timezone=timezone, title=title, + notifications=ScheduleNotifications(email=notifications_email) if notifications_email is not None else None, ) result = self._create(timeout=timeout, **schedule_fields.model_dump(by_alias=True, exclude_none=True)) return ScheduleResponse.model_validate(result).data @@ -231,6 +235,7 @@ async def create( description: str | None = None, timezone: str | None = None, title: str | None = None, + notifications_email: bool | None = None, timeout: Timeout = 'short', ) -> Schedule: """Create a new schedule. @@ -248,6 +253,7 @@ async def create( timezone: Timezone in which your cron expression runs (TZ database name from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). title: Title of this schedule. + notifications_email: Whether to send email notifications about the schedule's runs. timeout: Timeout for the API HTTP request. Returns: @@ -265,6 +271,7 @@ async def create( description=description, timezone=timezone, title=title, + notifications=ScheduleNotifications(email=notifications_email) if notifications_email is not None else None, ) result = await self._create(timeout=timeout, **schedule_fields.model_dump(by_alias=True, exclude_none=True)) return ScheduleResponse.model_validate(result).data diff --git a/src/apify_client/_resource_clients/store_collection.py b/src/apify_client/_resource_clients/store_collection.py index 7e5470e6..98f32852 100644 --- a/src/apify_client/_resource_clients/store_collection.py +++ b/src/apify_client/_resource_clients/store_collection.py @@ -43,6 +43,7 @@ def list( category: str | None = None, username: str | None = None, pricing_model: str | None = None, + include_unrunnable_actors: bool | None = None, timeout: Timeout = 'medium', ) -> ListOfStoreActors: """List Actors in Apify store. @@ -58,6 +59,9 @@ def list( category: Filter by this category. username: Filter by this username. pricing_model: Filter by this pricing model. + include_unrunnable_actors: Whether to include Actors that are not safe to run automatically, such as + Actors whose developers have not passed KYC, or full-permission Actors without a large user base. + By default, they are excluded. timeout: Timeout for the API HTTP request. Returns: @@ -72,6 +76,7 @@ def list( category=category, username=username, pricingModel=pricing_model, + includeUnrunnableActors=include_unrunnable_actors, ) return ListOfStoreActorsResponse.model_validate(result).data @@ -85,6 +90,7 @@ def iterate( category: str | None = None, username: str | None = None, pricing_model: str | None = None, + include_unrunnable_actors: bool | None = None, chunk_size: int | None = None, timeout: Timeout = 'medium', ) -> Iterator[StoreActor]: @@ -104,6 +110,9 @@ def iterate( category: Filter by this category. username: Filter by this username. pricing_model: Filter by this pricing model. + include_unrunnable_actors: Whether to include Actors that are not safe to run automatically, such as + Actors whose developers have not passed KYC, or full-permission Actors without a large user base. + By default, they are excluded. chunk_size: Maximum number of Actors requested per API call when iterating across pages. timeout: Timeout for the API HTTP request. @@ -120,6 +129,7 @@ def _callback(*, limit: int | None = None, offset: int | None = None) -> ListOfS category=category, username=username, pricing_model=pricing_model, + include_unrunnable_actors=include_unrunnable_actors, timeout=timeout, ) @@ -155,6 +165,7 @@ async def list( category: str | None = None, username: str | None = None, pricing_model: str | None = None, + include_unrunnable_actors: bool | None = None, timeout: Timeout = 'medium', ) -> ListOfStoreActors: """List Actors in Apify store. @@ -170,6 +181,9 @@ async def list( category: Filter by this category. username: Filter by this username. pricing_model: Filter by this pricing model. + include_unrunnable_actors: Whether to include Actors that are not safe to run automatically, such as + Actors whose developers have not passed KYC, or full-permission Actors without a large user base. + By default, they are excluded. timeout: Timeout for the API HTTP request. Returns: @@ -184,6 +198,7 @@ async def list( category=category, username=username, pricingModel=pricing_model, + includeUnrunnableActors=include_unrunnable_actors, ) return ListOfStoreActorsResponse.model_validate(result).data @@ -197,6 +212,7 @@ def iterate( category: str | None = None, username: str | None = None, pricing_model: str | None = None, + include_unrunnable_actors: bool | None = None, chunk_size: int | None = None, timeout: Timeout = 'medium', ) -> AsyncIterator[StoreActor]: @@ -216,6 +232,9 @@ def iterate( category: Filter by this category. username: Filter by this username. pricing_model: Filter by this pricing model. + include_unrunnable_actors: Whether to include Actors that are not safe to run automatically, such as + Actors whose developers have not passed KYC, or full-permission Actors without a large user base. + By default, they are excluded. chunk_size: Maximum number of Actors requested per API call when iterating across pages. timeout: Timeout for the API HTTP request. @@ -232,6 +251,7 @@ async def _callback(*, limit: int | None = None, offset: int | None = None) -> L category=category, username=username, pricing_model=pricing_model, + include_unrunnable_actors=include_unrunnable_actors, timeout=timeout, ) diff --git a/src/apify_client/_resource_clients/task.py b/src/apify_client/_resource_clients/task.py index 171be03a..f7819e8e 100644 --- a/src/apify_client/_resource_clients/task.py +++ b/src/apify_client/_resource_clients/task.py @@ -77,10 +77,12 @@ def update( task_input: TaskInputDict | TaskInput | None = None, build: str | None = None, max_items: int | None = None, + max_total_charge_usd: Decimal | None = None, memory_mbytes: int | None = None, run_timeout: timedelta | None = None, restart_on_error: bool | None = None, title: str | None = None, + description: 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, @@ -104,6 +106,7 @@ def update( the build specified in the task settings (typically latest). max_items: Maximum number of results that will be returned by this run. If the Actor is charged per result, you will not be charged for more results than the given limit. + max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified in the task settings. run_timeout: Optional timeout for the run. By default, the run uses timeout specified @@ -112,6 +115,7 @@ def update( a non-zero status code. task_input: Task input dictionary. title: A human-friendly equivalent of the name. + description: Description of the task. actor_standby_desired_requests_per_actor_run: The desired number of concurrent HTTP requests for a single Actor Standby run. actor_standby_max_requests_per_actor_run: The maximum number of concurrent HTTP requests for @@ -141,6 +145,7 @@ def update( task_fields = UpdateTaskRequest( name=name, title=title, + description=description, input=task_input, is_public=is_public, public_config=TaskPublicConfig( @@ -153,6 +158,7 @@ def update( options=TaskOptions( build=build, max_items=max_items, + max_total_charge_usd=max_total_charge_usd, memory_mbytes=memory_mbytes, timeout_secs=to_seconds(run_timeout, as_int=True), restart_on_error=restart_on_error, @@ -485,10 +491,12 @@ async def update( task_input: TaskInputDict | TaskInput | None = None, build: str | None = None, max_items: int | None = None, + max_total_charge_usd: Decimal | None = None, memory_mbytes: int | None = None, run_timeout: timedelta | None = None, restart_on_error: bool | None = None, title: str | None = None, + description: 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, @@ -512,6 +520,7 @@ async def update( the build specified in the task settings (typically latest). max_items: Maximum number of results that will be returned by this run. If the Actor is charged per result, you will not be charged for more results than the given limit. + max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified in the task settings. run_timeout: Optional timeout for the run. By default, the run uses timeout specified @@ -520,6 +529,7 @@ async def update( a non-zero status code. task_input: Task input dictionary. title: A human-friendly equivalent of the name. + description: Description of the task. actor_standby_desired_requests_per_actor_run: The desired number of concurrent HTTP requests for a single Actor Standby run. actor_standby_max_requests_per_actor_run: The maximum number of concurrent HTTP requests for @@ -549,6 +559,7 @@ async def update( task_fields = UpdateTaskRequest( name=name, title=title, + description=description, input=task_input, is_public=is_public, public_config=TaskPublicConfig( @@ -561,6 +572,7 @@ async def update( options=TaskOptions( build=build, max_items=max_items, + max_total_charge_usd=max_total_charge_usd, memory_mbytes=memory_mbytes, timeout_secs=to_seconds(run_timeout, as_int=True), restart_on_error=restart_on_error, diff --git a/src/apify_client/_resource_clients/task_collection.py b/src/apify_client/_resource_clients/task_collection.py index 24d73750..348efb52 100644 --- a/src/apify_client/_resource_clients/task_collection.py +++ b/src/apify_client/_resource_clients/task_collection.py @@ -21,6 +21,7 @@ if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterator from datetime import timedelta + from decimal import Decimal from apify_client._models import TaskListItem from apify_client._typeddicts import TaskInputDict @@ -111,9 +112,11 @@ def create( run_timeout: timedelta | None = None, memory_mbytes: int | None = None, max_items: int | None = None, + max_total_charge_usd: Decimal | None = None, restart_on_error: bool | None = None, task_input: TaskInputDict | TaskInput | None = None, title: str | None = None, + description: 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, @@ -142,12 +145,14 @@ def create( in the task settings. max_items: Maximum number of results that will be returned by runs of this task. If the Actor of this task is charged per result, you will not be charged for more results than the given limit. + max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. run_timeout: Optional timeout for the run. By default, the run uses timeout specified in the task settings. restart_on_error: If true, the Task run process will be restarted whenever it exits with a non-zero status code. task_input: Task input object. title: A human-friendly equivalent of the name. + description: Description of the task. actor_standby_desired_requests_per_actor_run: The desired number of concurrent HTTP requests for a single Actor Standby run. actor_standby_max_requests_per_actor_run: The maximum number of concurrent HTTP requests for @@ -175,6 +180,7 @@ def create( act_id=actor_id, name=name, title=title, + description=description, input=task_input, public_config=TaskPublicConfig( seo_title=public_config_seo_title, @@ -186,6 +192,7 @@ def create( options=TaskOptions( build=build, max_items=max_items, + max_total_charge_usd=max_total_charge_usd, memory_mbytes=memory_mbytes, timeout_secs=to_seconds(run_timeout, as_int=True), restart_on_error=restart_on_error, @@ -286,9 +293,11 @@ async def create( run_timeout: timedelta | None = None, memory_mbytes: int | None = None, max_items: int | None = None, + max_total_charge_usd: Decimal | None = None, restart_on_error: bool | None = None, task_input: TaskInputDict | TaskInput | None = None, title: str | None = None, + description: 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, @@ -317,12 +326,14 @@ async def create( in the task settings. max_items: Maximum number of results that will be returned by runs of this task. If the Actor of this task is charged per result, you will not be charged for more results than the given limit. + max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. run_timeout: Optional timeout for the run. By default, the run uses timeout specified in the task settings. restart_on_error: If true, the Task run process will be restarted whenever it exits with a non-zero status code. task_input: Task input object. title: A human-friendly equivalent of the name. + description: Description of the task. actor_standby_desired_requests_per_actor_run: The desired number of concurrent HTTP requests for a single Actor Standby run. actor_standby_max_requests_per_actor_run: The maximum number of concurrent HTTP requests for @@ -350,6 +361,7 @@ async def create( act_id=actor_id, name=name, title=title, + description=description, input=task_input, public_config=TaskPublicConfig( seo_title=public_config_seo_title, @@ -361,6 +373,7 @@ async def create( options=TaskOptions( build=build, max_items=max_items, + max_total_charge_usd=max_total_charge_usd, memory_mbytes=memory_mbytes, timeout_secs=to_seconds(run_timeout, as_int=True), restart_on_error=restart_on_error, diff --git a/src/apify_client/_resource_clients/webhook.py b/src/apify_client/_resource_clients/webhook.py index 096b3c18..93a4a584 100644 --- a/src/apify_client/_resource_clients/webhook.py +++ b/src/apify_client/_resource_clients/webhook.py @@ -66,11 +66,13 @@ def update( request_url: str | None = None, payload_template: str | None = None, headers_template: str | None = None, + should_interpolate_strings: bool | 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, + description: str | None = None, is_ad_hoc: bool | None = None, timeout: Timeout = 'short', ) -> WebhookResource: @@ -83,11 +85,14 @@ def update( request_url: URL that will be invoked once the webhook is triggered. payload_template: Specification of the payload that will be sent to request_url. headers_template: Headers that will be sent to the request_url. + should_interpolate_strings: Whether to also interpolate `{{...}}` variables inside string values of the + payload and headers templates. actor_id: Id of the Actor whose runs should trigger the webhook. actor_task_id: Id of the Actor task whose runs should trigger the webhook. actor_run_id: Id of the Actor run which should trigger the webhook. ignore_ssl_errors: Whether the webhook should ignore SSL errors returned by request_url. do_not_retry: Whether the webhook should retry sending the payload to request_url upon failure. + description: Description of the webhook. is_ad_hoc: Set to True if you want the webhook to be triggered only the first time the condition is fulfilled. Only applicable when actor_run_id is filled. timeout: Timeout for the API HTTP request. @@ -100,8 +105,10 @@ def update( request_url=AnyUrl(request_url) if request_url is not None else None, payload_template=payload_template, headers_template=headers_template, + should_interpolate_strings=should_interpolate_strings, ignore_ssl_errors=ignore_ssl_errors, do_not_retry=do_not_retry, + description=description, is_ad_hoc=is_ad_hoc if actor_run_id else None, condition=WebhookCondition( actor_run_id=actor_run_id, @@ -206,11 +213,13 @@ async def update( request_url: str | None = None, payload_template: str | None = None, headers_template: str | None = None, + should_interpolate_strings: bool | 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, + description: str | None = None, is_ad_hoc: bool | None = None, timeout: Timeout = 'short', ) -> WebhookResource: @@ -223,11 +232,14 @@ async def update( request_url: URL that will be invoked once the webhook is triggered. payload_template: Specification of the payload that will be sent to request_url. headers_template: Headers that will be sent to the request_url. + should_interpolate_strings: Whether to also interpolate `{{...}}` variables inside string values of the + payload and headers templates. actor_id: Id of the Actor whose runs should trigger the webhook. actor_task_id: Id of the Actor task whose runs should trigger the webhook. actor_run_id: Id of the Actor run which should trigger the webhook. ignore_ssl_errors: Whether the webhook should ignore SSL errors returned by request_url. do_not_retry: Whether the webhook should retry sending the payload to request_url upon failure. + description: Description of the webhook. is_ad_hoc: Set to True if you want the webhook to be triggered only the first time the condition is fulfilled. Only applicable when actor_run_id is filled. timeout: Timeout for the API HTTP request. @@ -240,8 +252,10 @@ async def update( request_url=AnyUrl(request_url) if request_url is not None else None, payload_template=payload_template, headers_template=headers_template, + should_interpolate_strings=should_interpolate_strings, ignore_ssl_errors=ignore_ssl_errors, do_not_retry=do_not_retry, + description=description, is_ad_hoc=is_ad_hoc if actor_run_id else None, condition=WebhookCondition( actor_run_id=actor_run_id, diff --git a/src/apify_client/_resource_clients/webhook_collection.py b/src/apify_client/_resource_clients/webhook_collection.py index 7cf06933..410dd8af 100644 --- a/src/apify_client/_resource_clients/webhook_collection.py +++ b/src/apify_client/_resource_clients/webhook_collection.py @@ -103,11 +103,13 @@ def create( request_url: str, payload_template: str | None = None, headers_template: str | None = None, + should_interpolate_strings: bool | 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, + description: str | None = None, idempotency_key: str | None = None, is_ad_hoc: bool | None = None, timeout: Timeout = 'short', @@ -123,11 +125,14 @@ def create( request_url: URL that will be invoked once the webhook is triggered. payload_template: Specification of the payload that will be sent to request_url. headers_template: Headers that will be sent to the request_url. + should_interpolate_strings: Whether to also interpolate `{{...}}` variables inside string values of the + payload and headers templates. actor_id: Id of the Actor whose runs should trigger the webhook. actor_task_id: Id of the Actor task whose runs should trigger the webhook. actor_run_id: Id of the Actor run which should trigger the webhook. ignore_ssl_errors: Whether the webhook should ignore SSL errors returned by request_url. do_not_retry: Whether the webhook should retry sending the payload to request_url upon failure. + description: Description of the webhook. idempotency_key: A unique identifier of a webhook. You can use it to ensure that you won't create the same webhook multiple times. is_ad_hoc: Set to True if you want the webhook to be triggered only the first time the condition @@ -142,8 +147,10 @@ def create( request_url=request_url, payload_template=payload_template, headers_template=headers_template, + should_interpolate_strings=should_interpolate_strings, ignore_ssl_errors=ignore_ssl_errors, do_not_retry=do_not_retry, + description=description, idempotency_key=idempotency_key, is_ad_hoc=is_ad_hoc if actor_run_id else None, condition=WebhookCondition( @@ -238,11 +245,13 @@ async def create( request_url: str, payload_template: str | None = None, headers_template: str | None = None, + should_interpolate_strings: bool | 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, + description: str | None = None, idempotency_key: str | None = None, is_ad_hoc: bool | None = None, timeout: Timeout = 'short', @@ -258,11 +267,14 @@ async def create( request_url: URL that will be invoked once the webhook is triggered. payload_template: Specification of the payload that will be sent to request_url. headers_template: Headers that will be sent to the request_url. + should_interpolate_strings: Whether to also interpolate `{{...}}` variables inside string values of the + payload and headers templates. actor_id: Id of the Actor whose runs should trigger the webhook. actor_task_id: Id of the Actor task whose runs should trigger the webhook. actor_run_id: Id of the Actor run which should trigger the webhook. ignore_ssl_errors: Whether the webhook should ignore SSL errors returned by request_url. do_not_retry: Whether the webhook should retry sending the payload to request_url upon failure. + description: Description of the webhook. idempotency_key: A unique identifier of a webhook. You can use it to ensure that you won't create the same webhook multiple times. is_ad_hoc: Set to True if you want the webhook to be triggered only the first time the condition @@ -277,8 +289,10 @@ async def create( request_url=request_url, payload_template=payload_template, headers_template=headers_template, + should_interpolate_strings=should_interpolate_strings, ignore_ssl_errors=ignore_ssl_errors, do_not_retry=do_not_retry, + description=description, idempotency_key=idempotency_key, is_ad_hoc=is_ad_hoc if actor_run_id else None, condition=WebhookCondition( diff --git a/tests/unit/test_actor_update_fields.py b/tests/unit/test_actor_update_fields.py index 9a7f8db2..d6060b0e 100644 --- a/tests/unit/test_actor_update_fields.py +++ b/tests/unit/test_actor_update_fields.py @@ -99,3 +99,27 @@ async def test_create_sends_standby_fields_async(httpserver: HTTPServer, async_c assert len(captured) == 1 assert body_subset(captured[0]) == _EXPECTED_BODY + + +def test_create_sends_actor_permission_level(httpserver: HTTPServer, sync_client: ApifyClient) -> None: + """`create` sends `actor_permission_level` as `actorPermissionLevel`.""" + captured: list[Request] = [] + httpserver.expect_request(_ACTORS_PATH, method='POST').respond_with_handler(capture(captured)) + + sync_client.actors().create(name='test-actor', actor_permission_level='LIMITED_PERMISSIONS') + + assert len(captured) == 1 + assert json.loads(captured[0].get_data()) == {'name': 'test-actor', 'actorPermissionLevel': 'LIMITED_PERMISSIONS'} + + +async def test_create_sends_actor_permission_level_async( + httpserver: HTTPServer, async_client: ApifyClientAsync +) -> None: + """Async `create` sends `actor_permission_level` as `actorPermissionLevel`.""" + captured: list[Request] = [] + httpserver.expect_request(_ACTORS_PATH, method='POST').respond_with_handler(capture(captured)) + + await async_client.actors().create(name='test-actor', actor_permission_level='LIMITED_PERMISSIONS') + + assert len(captured) == 1 + assert json.loads(captured[0].get_data()) == {'name': 'test-actor', 'actorPermissionLevel': 'LIMITED_PERMISSIONS'} diff --git a/tests/unit/test_dataset_item_params.py b/tests/unit/test_dataset_item_params.py new file mode 100644 index 00000000..b9821369 --- /dev/null +++ b/tests/unit/test_dataset_item_params.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +import json +from typing import TYPE_CHECKING + +import pytest +from werkzeug import Request, Response + +if TYPE_CHECKING: + from collections.abc import Callable + + from pytest_httpserver import HTTPServer + + from apify_client import ApifyClient, ApifyClientAsync + +pytestmark = pytest.mark.usefixtures('http_client_classes') + +_DATASET_ID = 'test-dataset-id' +_ITEMS_PATH = f'/v2/datasets/{_DATASET_ID}/items' + + +def capture(captured: list[Request]) -> Callable[[Request], Response]: + def handler(request: Request) -> Response: + captured.append(request) + return Response( + status=200, + headers={ + 'x-apify-pagination-total': '1', + 'x-apify-pagination-offset': '0', + 'x-apify-pagination-count': '1', + 'x-apify-pagination-limit': '999999999999', + 'x-apify-pagination-desc': 'false', + 'content-type': 'application/json', + }, + response=json.dumps([{'id': 1}]), + ) + + return handler + + +def test_iterate_items_sends_flatten_and_view(httpserver: HTTPServer, sync_client: ApifyClient) -> None: + """`iterate_items` forwards `flatten` and `view` to the page requests.""" + captured: list[Request] = [] + httpserver.expect_request(_ITEMS_PATH, method='GET').respond_with_handler(capture(captured)) + + items = list(sync_client.dataset(_DATASET_ID).iterate_items(limit=1, flatten=['a', 'b'], view='overview')) + + assert items == [{'id': 1}] + assert captured[0].args.get('flatten') == 'a,b' + assert captured[0].args.get('view') == 'overview' + + +async def test_iterate_items_sends_flatten_and_view_async( + httpserver: HTTPServer, async_client: ApifyClientAsync +) -> None: + """Async `iterate_items` forwards `flatten` and `view` to the page requests.""" + captured: list[Request] = [] + httpserver.expect_request(_ITEMS_PATH, method='GET').respond_with_handler(capture(captured)) + + dataset = async_client.dataset(_DATASET_ID) + items = [item async for item in dataset.iterate_items(limit=1, flatten=['a', 'b'], view='overview')] + + assert items == [{'id': 1}] + assert captured[0].args.get('flatten') == 'a,b' + assert captured[0].args.get('view') == 'overview' + + +def test_get_items_as_bytes_sends_view_and_attachment(httpserver: HTTPServer, sync_client: ApifyClient) -> None: + """`get_items_as_bytes` sends `view` and `attachment` as query parameters.""" + captured: list[Request] = [] + httpserver.expect_request(_ITEMS_PATH, method='GET').respond_with_handler(capture(captured)) + + sync_client.dataset(_DATASET_ID).get_items_as_bytes(view='overview', attachment=True) + + assert len(captured) == 1 + assert captured[0].args.get('view') == 'overview' + assert captured[0].args.get('attachment') == 'true' + + +async def test_get_items_as_bytes_sends_view_and_attachment_async( + httpserver: HTTPServer, async_client: ApifyClientAsync +) -> None: + """Async `get_items_as_bytes` sends `view` and `attachment` as query parameters.""" + captured: list[Request] = [] + httpserver.expect_request(_ITEMS_PATH, method='GET').respond_with_handler(capture(captured)) + + await async_client.dataset(_DATASET_ID).get_items_as_bytes(view='overview', attachment=True) + + assert len(captured) == 1 + assert captured[0].args.get('view') == 'overview' + assert captured[0].args.get('attachment') == 'true' + + +def test_stream_items_sends_view_and_attachment(httpserver: HTTPServer, sync_client: ApifyClient) -> None: + """`stream_items` sends `view` and `attachment` as query parameters.""" + captured: list[Request] = [] + httpserver.expect_request(_ITEMS_PATH, method='GET').respond_with_handler(capture(captured)) + + with sync_client.dataset(_DATASET_ID).stream_items(view='overview', attachment=True): + pass + + assert len(captured) == 1 + assert captured[0].args.get('view') == 'overview' + assert captured[0].args.get('attachment') == 'true' + + +async def test_stream_items_sends_view_and_attachment_async( + httpserver: HTTPServer, async_client: ApifyClientAsync +) -> None: + """Async `stream_items` sends `view` and `attachment` as query parameters.""" + captured: list[Request] = [] + httpserver.expect_request(_ITEMS_PATH, method='GET').respond_with_handler(capture(captured)) + + async with async_client.dataset(_DATASET_ID).stream_items(view='overview', attachment=True): + pass + + assert len(captured) == 1 + assert captured[0].args.get('view') == 'overview' + assert captured[0].args.get('attachment') == 'true' diff --git a/tests/unit/test_schedule_notifications.py b/tests/unit/test_schedule_notifications.py new file mode 100644 index 00000000..e1342599 --- /dev/null +++ b/tests/unit/test_schedule_notifications.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +import json +from typing import TYPE_CHECKING, Any + +import pytest +from werkzeug import Request, Response + +if TYPE_CHECKING: + from collections.abc import Callable + + from pytest_httpserver import HTTPServer + + from apify_client import ApifyClient, ApifyClientAsync + +pytestmark = pytest.mark.usefixtures('http_client_classes') + +_MOCKED_SCHEDULE_ID = 'test_schedule_id' +_SCHEDULE_PATH = f'/v2/schedules/{_MOCKED_SCHEDULE_ID}' +_SCHEDULES_PATH = '/v2/schedules' + +_SCHEDULE_RESPONSE = { + 'data': { + 'id': _MOCKED_SCHEDULE_ID, + 'userId': 'test_user_id', + 'name': 'test-schedule', + 'cronExpression': '@daily', + 'timezone': 'UTC', + 'isEnabled': True, + 'isExclusive': False, + 'createdAt': '2026-08-01T10:00:00.000Z', + 'modifiedAt': '2026-08-01T10:00:00.000Z', + 'actions': [], + 'notifications': {'email': False}, + } +} + + +def capture(captured: list[Request]) -> Callable[[Request], Response]: + def handler(request: Request) -> Response: + captured.append(request) + return Response(json.dumps(_SCHEDULE_RESPONSE), status=200, mimetype='application/json') + + return handler + + +def body_of(request: Request) -> dict[str, Any]: + return json.loads(request.get_data()) + + +@pytest.mark.parametrize( + 'notifications_email', + [ + pytest.param(True, id='enabled'), + pytest.param(False, id='disabled'), + ], +) +def test_update_sends_notifications( + httpserver: HTTPServer, sync_client: ApifyClient, *, notifications_email: bool +) -> None: + """`update` sends `notifications_email` as `notifications.email`.""" + captured: list[Request] = [] + httpserver.expect_request(_SCHEDULE_PATH, method='PUT').respond_with_handler(capture(captured)) + + sync_client.schedule(_MOCKED_SCHEDULE_ID).update(notifications_email=notifications_email) + + assert len(captured) == 1 + assert body_of(captured[0]) == {'notifications': {'email': notifications_email}} + + +@pytest.mark.parametrize( + 'notifications_email', + [ + pytest.param(True, id='enabled'), + pytest.param(False, id='disabled'), + ], +) +async def test_update_sends_notifications_async( + httpserver: HTTPServer, async_client: ApifyClientAsync, *, notifications_email: bool +) -> None: + """Async `update` sends `notifications_email` as `notifications.email`.""" + captured: list[Request] = [] + httpserver.expect_request(_SCHEDULE_PATH, method='PUT').respond_with_handler(capture(captured)) + + await async_client.schedule(_MOCKED_SCHEDULE_ID).update(notifications_email=notifications_email) + + assert len(captured) == 1 + assert body_of(captured[0]) == {'notifications': {'email': notifications_email}} + + +def test_update_omits_notifications_by_default(httpserver: HTTPServer, sync_client: ApifyClient) -> None: + """`update` sends no `notifications` key unless `notifications_email` is passed.""" + captured: list[Request] = [] + httpserver.expect_request(_SCHEDULE_PATH, method='PUT').respond_with_handler(capture(captured)) + + sync_client.schedule(_MOCKED_SCHEDULE_ID).update(name='renamed') + + assert len(captured) == 1 + assert body_of(captured[0]) == {'name': 'renamed'} + + +def test_create_sends_notifications(httpserver: HTTPServer, sync_client: ApifyClient) -> None: + """`create` sends `notifications_email` as `notifications.email`.""" + captured: list[Request] = [] + httpserver.expect_request(_SCHEDULES_PATH, method='POST').respond_with_handler(capture(captured)) + + sync_client.schedules().create( + cron_expression='@daily', + is_enabled=True, + is_exclusive=False, + notifications_email=True, + ) + + assert len(captured) == 1 + assert body_of(captured[0])['notifications'] == {'email': True} + + +async def test_create_sends_notifications_async(httpserver: HTTPServer, async_client: ApifyClientAsync) -> None: + """Async `create` sends `notifications_email` as `notifications.email`.""" + captured: list[Request] = [] + httpserver.expect_request(_SCHEDULES_PATH, method='POST').respond_with_handler(capture(captured)) + + await async_client.schedules().create( + cron_expression='@daily', + is_enabled=True, + is_exclusive=False, + notifications_email=True, + ) + + assert len(captured) == 1 + assert body_of(captured[0])['notifications'] == {'email': True} diff --git a/tests/unit/test_storage_update_title.py b/tests/unit/test_storage_update_title.py new file mode 100644 index 00000000..8cf4cdd4 --- /dev/null +++ b/tests/unit/test_storage_update_title.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import json +from typing import TYPE_CHECKING, Any + +import pytest +from werkzeug import Request, Response + +if TYPE_CHECKING: + from collections.abc import Callable + + from pytest_httpserver import HTTPServer + + from apify_client import ApifyClient, ApifyClientAsync + +pytestmark = pytest.mark.usefixtures('http_client_classes') + +_STORAGE_ID = 'test_storage_id' + +_STORAGE_RESPONSE = { + 'data': { + 'id': _STORAGE_ID, + 'userId': 'test_user_id', + 'createdAt': '2026-08-01T10:00:00.000Z', + 'modifiedAt': '2026-08-01T10:00:00.000Z', + 'accessedAt': '2026-08-01T10:00:00.000Z', + 'itemCount': 0, + 'cleanItemCount': 0, + 'totalRequestCount': 0, + 'handledRequestCount': 0, + 'pendingRequestCount': 0, + 'hadMultipleClients': False, + 'consoleUrl': 'https://console.apify.com/storage/test_storage_id', + } +} + +_STORAGES = [ + pytest.param('datasets', 'dataset', id='dataset'), + pytest.param('key-value-stores', 'key_value_store', id='key-value store'), + pytest.param('request-queues', 'request_queue', id='request queue'), +] + + +def capture(captured: list[Request]) -> Callable[[Request], Response]: + def handler(request: Request) -> Response: + captured.append(request) + return Response(json.dumps(_STORAGE_RESPONSE), status=200, mimetype='application/json') + + return handler + + +def body_of(request: Request) -> dict[str, Any]: + return json.loads(request.get_data()) + + +@pytest.mark.parametrize(('resource_path', 'client_method'), _STORAGES) +def test_update_sends_title( + httpserver: HTTPServer, sync_client: ApifyClient, resource_path: str, client_method: str +) -> None: + """`update` of each storage sends `title`.""" + captured: list[Request] = [] + httpserver.expect_request(f'/v2/{resource_path}/{_STORAGE_ID}', method='PUT').respond_with_handler( + capture(captured) + ) + + getattr(sync_client, client_method)(_STORAGE_ID).update(title='My storage') + + assert len(captured) == 1 + assert body_of(captured[0]) == {'title': 'My storage'} + + +@pytest.mark.parametrize(('resource_path', 'client_method'), _STORAGES) +async def test_update_sends_title_async( + httpserver: HTTPServer, async_client: ApifyClientAsync, resource_path: str, client_method: str +) -> None: + """Async `update` of each storage sends `title`.""" + captured: list[Request] = [] + httpserver.expect_request(f'/v2/{resource_path}/{_STORAGE_ID}', method='PUT').respond_with_handler( + capture(captured) + ) + + await getattr(async_client, client_method)(_STORAGE_ID).update(title='My storage') + + assert len(captured) == 1 + assert body_of(captured[0]) == {'title': 'My storage'} diff --git a/tests/unit/test_store_collection_params.py b/tests/unit/test_store_collection_params.py new file mode 100644 index 00000000..d660bcac --- /dev/null +++ b/tests/unit/test_store_collection_params.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import json +from typing import TYPE_CHECKING + +import pytest +from werkzeug import Request, Response + +if TYPE_CHECKING: + from collections.abc import Callable + + from pytest_httpserver import HTTPServer + + from apify_client import ApifyClient, ApifyClientAsync + +pytestmark = pytest.mark.usefixtures('http_client_classes') + +_STORE_PATH = '/v2/store' + +_STORE_RESPONSE = { + 'data': { + 'total': 0, + 'offset': 0, + 'limit': 1000, + 'desc': False, + 'count': 0, + 'items': [], + } +} + + +def capture(captured: list[Request]) -> Callable[[Request], Response]: + def handler(request: Request) -> Response: + captured.append(request) + return Response(json.dumps(_STORE_RESPONSE), status=200, mimetype='application/json') + + return handler + + +def test_list_sends_include_unrunnable_actors(httpserver: HTTPServer, sync_client: ApifyClient) -> None: + """`list` sends `include_unrunnable_actors` as the `includeUnrunnableActors` query parameter.""" + captured: list[Request] = [] + httpserver.expect_request(_STORE_PATH, method='GET').respond_with_handler(capture(captured)) + + sync_client.store().list(include_unrunnable_actors=True) + + assert len(captured) == 1 + assert captured[0].args.get('includeUnrunnableActors') == 'true' + + +async def test_list_sends_include_unrunnable_actors_async( + httpserver: HTTPServer, async_client: ApifyClientAsync +) -> None: + """Async `list` sends `include_unrunnable_actors` as the `includeUnrunnableActors` query parameter.""" + captured: list[Request] = [] + httpserver.expect_request(_STORE_PATH, method='GET').respond_with_handler(capture(captured)) + + await async_client.store().list(include_unrunnable_actors=True) + + assert len(captured) == 1 + assert captured[0].args.get('includeUnrunnableActors') == 'true' + + +def test_iterate_sends_include_unrunnable_actors(httpserver: HTTPServer, sync_client: ApifyClient) -> None: + """`iterate` forwards `include_unrunnable_actors` to every page request.""" + captured: list[Request] = [] + httpserver.expect_request(_STORE_PATH, method='GET').respond_with_handler(capture(captured)) + + list(sync_client.store().iterate(include_unrunnable_actors=True)) + + assert len(captured) == 1 + assert captured[0].args.get('includeUnrunnableActors') == 'true' + + +async def test_iterate_sends_include_unrunnable_actors_async( + httpserver: HTTPServer, async_client: ApifyClientAsync +) -> None: + """Async `iterate` forwards `include_unrunnable_actors` to every page request.""" + captured: list[Request] = [] + httpserver.expect_request(_STORE_PATH, method='GET').respond_with_handler(capture(captured)) + + actors = [actor async for actor in async_client.store().iterate(include_unrunnable_actors=True)] + + assert actors == [] + assert len(captured) == 1 + assert captured[0].args.get('includeUnrunnableActors') == 'true' diff --git a/tests/unit/test_task_update_fields.py b/tests/unit/test_task_update_fields.py new file mode 100644 index 00000000..2ab0a7d6 --- /dev/null +++ b/tests/unit/test_task_update_fields.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import json +from decimal import Decimal +from typing import TYPE_CHECKING, Any + +import pytest +from werkzeug import Request, Response + +if TYPE_CHECKING: + from collections.abc import Callable + + from pytest_httpserver import HTTPServer + + from apify_client import ApifyClient, ApifyClientAsync + +pytestmark = pytest.mark.usefixtures('http_client_classes') + +_MOCKED_TASK_ID = 'test_task_id' +_TASK_PATH = f'/v2/actor-tasks/{_MOCKED_TASK_ID}' +_TASKS_PATH = '/v2/actor-tasks' + +_TASK_RESPONSE = { + 'data': { + 'id': _MOCKED_TASK_ID, + 'userId': 'test_user_id', + 'actId': 'test_actor_id', + 'name': 'test-task', + 'createdAt': '2026-08-01T10:00:00.000Z', + 'modifiedAt': '2026-08-01T10:00:00.000Z', + } +} + +_FIELDS: dict[str, Any] = { + 'description': 'Scrapes the front page.', + 'max_total_charge_usd': Decimal('2.5'), +} + +_EXPECTED_BODY = { + 'description': 'Scrapes the front page.', + 'options': {'maxTotalChargeUsd': 2.5}, +} + + +def capture(captured: list[Request]) -> Callable[[Request], Response]: + def handler(request: Request) -> Response: + captured.append(request) + return Response(json.dumps(_TASK_RESPONSE), status=200, mimetype='application/json') + + return handler + + +def body_subset(request: Request) -> dict[str, Any]: + body = json.loads(request.get_data()) + return {key: body[key] for key in _EXPECTED_BODY} + + +def test_update_sends_description_and_max_total_charge(httpserver: HTTPServer, sync_client: ApifyClient) -> None: + """`update` sends `description` and nests `max_total_charge_usd` into `options`.""" + captured: list[Request] = [] + httpserver.expect_request(_TASK_PATH, method='PUT').respond_with_handler(capture(captured)) + + sync_client.task(_MOCKED_TASK_ID).update(**_FIELDS) + + assert len(captured) == 1 + assert body_subset(captured[0]) == _EXPECTED_BODY + + +async def test_update_sends_description_and_max_total_charge_async( + httpserver: HTTPServer, async_client: ApifyClientAsync +) -> None: + """Async `update` sends `description` and nests `max_total_charge_usd` into `options`.""" + captured: list[Request] = [] + httpserver.expect_request(_TASK_PATH, method='PUT').respond_with_handler(capture(captured)) + + await async_client.task(_MOCKED_TASK_ID).update(**_FIELDS) + + assert len(captured) == 1 + assert body_subset(captured[0]) == _EXPECTED_BODY + + +def test_create_sends_description_and_max_total_charge(httpserver: HTTPServer, sync_client: ApifyClient) -> None: + """`create` sends `description` and nests `max_total_charge_usd` into `options`.""" + captured: list[Request] = [] + httpserver.expect_request(_TASKS_PATH, method='POST').respond_with_handler(capture(captured)) + + sync_client.tasks().create(actor_id='test_actor_id', name='test-task', **_FIELDS) + + assert len(captured) == 1 + assert body_subset(captured[0]) == _EXPECTED_BODY + + +async def test_create_sends_description_and_max_total_charge_async( + httpserver: HTTPServer, async_client: ApifyClientAsync +) -> None: + """Async `create` sends `description` and nests `max_total_charge_usd` into `options`.""" + captured: list[Request] = [] + httpserver.expect_request(_TASKS_PATH, method='POST').respond_with_handler(capture(captured)) + + await async_client.tasks().create(actor_id='test_actor_id', name='test-task', **_FIELDS) + + assert len(captured) == 1 + assert body_subset(captured[0]) == _EXPECTED_BODY diff --git a/tests/unit/test_url_generation.py b/tests/unit/test_url_generation.py index 5e05b66a..fc003118 100644 --- a/tests/unit/test_url_generation.py +++ b/tests/unit/test_url_generation.py @@ -156,6 +156,36 @@ async def test_dataset_public_url_normalizes_params_async() -> None: assert query['fields'] == ['title,url'] +def test_dataset_public_url_sends_item_format_sync() -> None: + """`item_format` becomes the `format` query parameter of the public URL.""" + client = ApifyClient(token='dummy-token', api_url='https://api.apify.com') + dataset = client.dataset('someID') + + mock_response = Mock() + mock_response.json.return_value = json.loads(MOCKED_DATASET_RESPONSE) + + with mock.patch.object(client._http_client, 'call', return_value=mock_response): + public_url = dataset.create_items_public_url(item_format='csv') + + query = parse_qs(urlparse(public_url).query) + assert query['format'] == ['csv'] + + +async def test_dataset_public_url_sends_item_format_async() -> None: + """`item_format` becomes the `format` query parameter of the public URL.""" + client = ApifyClientAsync(token='dummy-token', api_url='https://api.apify.com') + dataset = client.dataset('someID') + + mock_response = Mock() + mock_response.json.return_value = json.loads(MOCKED_DATASET_RESPONSE) + + with mock.patch.object(client._http_client, 'call', return_value=mock_response): + public_url = await dataset.create_items_public_url(item_format='csv') + + query = parse_qs(urlparse(public_url).query) + assert query['format'] == ['csv'] + + @pytest.mark.parametrize('signing_key', [None, 'custom-signing-key']) @parametrized_api_urls def test_kvs_public_url_sync(api_url: str, api_public_url: str | None, signing_key: str | None) -> None: diff --git a/tests/unit/test_webhook_fields.py b/tests/unit/test_webhook_fields.py new file mode 100644 index 00000000..6e573344 --- /dev/null +++ b/tests/unit/test_webhook_fields.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import json +from typing import TYPE_CHECKING, Any + +import pytest +from werkzeug import Request, Response + +if TYPE_CHECKING: + from collections.abc import Callable + + from pytest_httpserver import HTTPServer + + from apify_client import ApifyClient, ApifyClientAsync + +pytestmark = pytest.mark.usefixtures('http_client_classes') + +_MOCKED_WEBHOOK_ID = 'test_webhook_id' +_WEBHOOK_PATH = f'/v2/webhooks/{_MOCKED_WEBHOOK_ID}' +_WEBHOOKS_PATH = '/v2/webhooks' + +_WEBHOOK_RESPONSE = { + 'data': { + 'id': _MOCKED_WEBHOOK_ID, + 'userId': 'test_user_id', + 'createdAt': '2026-08-01T10:00:00.000Z', + 'modifiedAt': '2026-08-01T10:00:00.000Z', + 'eventTypes': ['ACTOR.RUN.SUCCEEDED'], + 'condition': {'actorId': 'test_actor_id'}, + 'ignoreSslErrors': False, + } +} + +_FIELDS: dict[str, Any] = { + 'description': 'Notifies the CRM.', + 'should_interpolate_strings': True, +} + +_EXPECTED_BODY = { + 'description': 'Notifies the CRM.', + 'shouldInterpolateStrings': True, +} + + +def capture(captured: list[Request]) -> Callable[[Request], Response]: + def handler(request: Request) -> Response: + captured.append(request) + return Response(json.dumps(_WEBHOOK_RESPONSE), status=200, mimetype='application/json') + + return handler + + +def body_subset(request: Request) -> dict[str, Any]: + body = json.loads(request.get_data()) + return {key: body[key] for key in _EXPECTED_BODY} + + +def test_update_sends_description_and_interpolation_flag(httpserver: HTTPServer, sync_client: ApifyClient) -> None: + """`update` sends `description` and `shouldInterpolateStrings`.""" + captured: list[Request] = [] + httpserver.expect_request(_WEBHOOK_PATH, method='PUT').respond_with_handler(capture(captured)) + + sync_client.webhook(_MOCKED_WEBHOOK_ID).update(**_FIELDS) + + assert len(captured) == 1 + assert body_subset(captured[0]) == _EXPECTED_BODY + + +async def test_update_sends_description_and_interpolation_flag_async( + httpserver: HTTPServer, async_client: ApifyClientAsync +) -> None: + """Async `update` sends `description` and `shouldInterpolateStrings`.""" + captured: list[Request] = [] + httpserver.expect_request(_WEBHOOK_PATH, method='PUT').respond_with_handler(capture(captured)) + + await async_client.webhook(_MOCKED_WEBHOOK_ID).update(**_FIELDS) + + assert len(captured) == 1 + assert body_subset(captured[0]) == _EXPECTED_BODY + + +def test_create_sends_description_and_interpolation_flag(httpserver: HTTPServer, sync_client: ApifyClient) -> None: + """`create` sends `description` and `shouldInterpolateStrings`.""" + captured: list[Request] = [] + httpserver.expect_request(_WEBHOOKS_PATH, method='POST').respond_with_handler(capture(captured)) + + sync_client.webhooks().create( + event_types=['ACTOR.RUN.SUCCEEDED'], + request_url='https://example.com/hook', + actor_id='test_actor_id', + **_FIELDS, + ) + + assert len(captured) == 1 + assert body_subset(captured[0]) == _EXPECTED_BODY + + +async def test_create_sends_description_and_interpolation_flag_async( + httpserver: HTTPServer, async_client: ApifyClientAsync +) -> None: + """Async `create` sends `description` and `shouldInterpolateStrings`.""" + captured: list[Request] = [] + httpserver.expect_request(_WEBHOOKS_PATH, method='POST').respond_with_handler(capture(captured)) + + await async_client.webhooks().create( + event_types=['ACTOR.RUN.SUCCEEDED'], + request_url='https://example.com/hook', + actor_id='test_actor_id', + **_FIELDS, + ) + + assert len(captured) == 1 + assert body_subset(captured[0]) == _EXPECTED_BODY