diff --git a/src/apify_client/_resource_clients/task.py b/src/apify_client/_resource_clients/task.py index 90f8e57e..171be03a 100644 --- a/src/apify_client/_resource_clients/task.py +++ b/src/apify_client/_resource_clients/task.py @@ -22,6 +22,7 @@ if TYPE_CHECKING: from datetime import timedelta + from decimal import Decimal from apify_client._literals import ActorJobStatus, RunOrigin from apify_client._resource_clients import ( @@ -217,6 +218,7 @@ def start( 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, @@ -235,6 +237,7 @@ def start( the run uses 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 @@ -268,6 +271,7 @@ def start( request_params = self._build_params( build=build, maxItems=max_items, + maxTotalChargeUsd=max_total_charge_usd, memory=memory_mbytes, timeout=to_seconds(run_timeout, as_int=True), restartOnError=restart_on_error, @@ -296,6 +300,7 @@ def call( 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, @@ -316,6 +321,7 @@ def call( the run uses 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 @@ -344,6 +350,7 @@ def call( task_input=task_input, build=build, max_items=max_items, + max_total_charge_usd=max_total_charge_usd, memory_mbytes=memory_mbytes, run_timeout=run_timeout, restart_on_error=restart_on_error, @@ -619,6 +626,7 @@ async def start( 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, @@ -637,6 +645,7 @@ async def start( the run uses 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 @@ -670,6 +679,7 @@ async def start( request_params = self._build_params( build=build, maxItems=max_items, + maxTotalChargeUsd=max_total_charge_usd, memory=memory_mbytes, timeout=to_seconds(run_timeout, as_int=True), restartOnError=restart_on_error, @@ -698,6 +708,7 @@ async def call( 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, @@ -718,6 +729,7 @@ async def call( the run uses 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 @@ -746,6 +758,7 @@ async def call( task_input=task_input, build=build, max_items=max_items, + max_total_charge_usd=max_total_charge_usd, memory_mbytes=memory_mbytes, run_timeout=run_timeout, restart_on_error=restart_on_error, diff --git a/tests/unit/test_task_start_params.py b/tests/unit/test_task_start_params.py new file mode 100644 index 00000000..4485db26 --- /dev/null +++ b/tests/unit/test_task_start_params.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import inspect +from decimal import Decimal +from typing import TYPE_CHECKING, Any + +import pytest + +from apify_client import ApifyClient, ApifyClientAsync + +if TYPE_CHECKING: + from collections.abc import Callable + + from pytest_httpserver import HTTPServer + +pytestmark = pytest.mark.usefixtures('http_client_classes') + +RUN = { + 'id': 'run-id', + 'actId': 'actor-id', + 'userId': 'user-id', + 'startedAt': '2019-11-30T07:34:24.202Z', + 'status': 'SUCCEEDED', + 'meta': {'origin': 'API'}, + 'stats': {'restartCount': 0, 'resurrectCount': 0, 'computeUnits': 0.1}, + 'options': {'build': 'latest', 'timeoutSecs': 300, 'memoryMbytes': 1024, 'diskMbytes': 2048}, + 'buildId': 'build-id', + 'generalAccess': 'RESTRICTED', + 'defaultKeyValueStoreId': 'kvs-id', + 'defaultDatasetId': 'dataset-id', + 'defaultRequestQueueId': 'rq-id', + 'buildNumber': '0.0.1', + 'containerUrl': 'https://test.runs.apify.net', +} + +STARTERS = [ + pytest.param(lambda c, **kw: c.task('task-id').start(**kw), id='start'), + pytest.param(lambda c, **kw: c.task('task-id').call(**kw), id='call'), +] + + +@pytest.fixture(params=[pytest.param(ApifyClient, id='sync'), pytest.param(ApifyClientAsync, id='async')]) +def client(request: pytest.FixtureRequest, httpserver: HTTPServer) -> ApifyClient | ApifyClientAsync: + return request.param(token='test', api_url=httpserver.url_for('/').removesuffix('/')) + + +async def run(starter: Callable[..., Any], client: ApifyClient | ApifyClientAsync, **kwargs: Any) -> Any: + result = starter(client, **kwargs) + return await result if inspect.isawaitable(result) else result + + +@pytest.mark.parametrize('starter', STARTERS) +@pytest.mark.parametrize( + ('max_total_charge_usd', 'expected_query'), + [ + pytest.param(Decimal('1.5'), {'maxTotalChargeUsd': '1.5'}, id='set'), + pytest.param(None, {}, id='omitted'), + ], +) +async def test_task_run_sends_max_total_charge_usd( + httpserver: HTTPServer, + client: ApifyClient | ApifyClientAsync, + starter: Callable[..., Any], + max_total_charge_usd: Decimal | None, + expected_query: dict[str, str], +) -> None: + """`max_total_charge_usd` reaches the task run API as the `maxTotalChargeUsd` query parameter.""" + httpserver.expect_request( + '/v2/actor-tasks/task-id/runs', method='POST', query_string=expected_query + ).respond_with_json({'data': RUN}, status=201) + httpserver.expect_request('/v2/actor-runs/run-id').respond_with_json({'data': RUN}) + + await run(starter, client, max_total_charge_usd=max_total_charge_usd) + + httpserver.check_assertions()