Skip to content

Commit 966d3d6

Browse files
authored
feat: add max_total_charge_usd to task start() and call() (#1097)
`TaskClient.start()` and `call()`, sync and async, now accept `max_total_charge_usd` and send it as the `maxTotalChargeUsd` query parameter, matching `ActorClient` and the JS client's `task.start()`. The SDK needs this for apify/apify-sdk-python#1155 so that named task child runs can share the parent's charge budget. Closes: #1096 *✍️ Drafted by Claude Code*
1 parent 43f1d5a commit 966d3d6

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

‎src/apify_client/_resource_clients/task.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
if TYPE_CHECKING:
2424
from datetime import timedelta
25+
from decimal import Decimal
2526

2627
from apify_client._literals import ActorJobStatus, RunOrigin
2728
from apify_client._resource_clients import (
@@ -217,6 +218,7 @@ def start(
217218
task_input: TaskInputDict | TaskInput | None = None,
218219
build: str | None = None,
219220
max_items: int | None = None,
221+
max_total_charge_usd: Decimal | None = None,
220222
memory_mbytes: int | None = None,
221223
run_timeout: timedelta | None = None,
222224
restart_on_error: bool | None = None,
@@ -235,6 +237,7 @@ def start(
235237
the run uses the build specified in the task settings (typically latest).
236238
max_items: Maximum number of results that will be returned by this run. If the Actor is charged
237239
per result, you will not be charged for more results than the given limit.
240+
max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors.
238241
memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified
239242
in the task settings.
240243
run_timeout: Optional timeout for the run. By default, the run uses timeout specified
@@ -268,6 +271,7 @@ def start(
268271
request_params = self._build_params(
269272
build=build,
270273
maxItems=max_items,
274+
maxTotalChargeUsd=max_total_charge_usd,
271275
memory=memory_mbytes,
272276
timeout=to_seconds(run_timeout, as_int=True),
273277
restartOnError=restart_on_error,
@@ -296,6 +300,7 @@ def call(
296300
task_input: TaskInputDict | TaskInput | None = None,
297301
build: str | None = None,
298302
max_items: int | None = None,
303+
max_total_charge_usd: Decimal | None = None,
299304
memory_mbytes: int | None = None,
300305
run_timeout: timedelta | None = None,
301306
restart_on_error: bool | None = None,
@@ -316,6 +321,7 @@ def call(
316321
the run uses the build specified in the task settings (typically latest).
317322
max_items: Maximum number of results that will be returned by this run. If the Actor is charged per result,
318323
you will not be charged for more results than the given limit.
324+
max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors.
319325
memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified
320326
in the task settings.
321327
run_timeout: Optional timeout for the run. By default, the run uses timeout specified
@@ -344,6 +350,7 @@ def call(
344350
task_input=task_input,
345351
build=build,
346352
max_items=max_items,
353+
max_total_charge_usd=max_total_charge_usd,
347354
memory_mbytes=memory_mbytes,
348355
run_timeout=run_timeout,
349356
restart_on_error=restart_on_error,
@@ -619,6 +626,7 @@ async def start(
619626
task_input: TaskInputDict | TaskInput | None = None,
620627
build: str | None = None,
621628
max_items: int | None = None,
629+
max_total_charge_usd: Decimal | None = None,
622630
memory_mbytes: int | None = None,
623631
run_timeout: timedelta | None = None,
624632
restart_on_error: bool | None = None,
@@ -637,6 +645,7 @@ async def start(
637645
the run uses the build specified in the task settings (typically latest).
638646
max_items: Maximum number of results that will be returned by this run. If the Actor is charged
639647
per result, you will not be charged for more results than the given limit.
648+
max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors.
640649
memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified
641650
in the task settings.
642651
run_timeout: Optional timeout for the run. By default, the run uses timeout specified
@@ -670,6 +679,7 @@ async def start(
670679
request_params = self._build_params(
671680
build=build,
672681
maxItems=max_items,
682+
maxTotalChargeUsd=max_total_charge_usd,
673683
memory=memory_mbytes,
674684
timeout=to_seconds(run_timeout, as_int=True),
675685
restartOnError=restart_on_error,
@@ -698,6 +708,7 @@ async def call(
698708
task_input: TaskInputDict | TaskInput | None = None,
699709
build: str | None = None,
700710
max_items: int | None = None,
711+
max_total_charge_usd: Decimal | None = None,
701712
memory_mbytes: int | None = None,
702713
run_timeout: timedelta | None = None,
703714
restart_on_error: bool | None = None,
@@ -718,6 +729,7 @@ async def call(
718729
the run uses the build specified in the task settings (typically latest).
719730
max_items: Maximum number of results that will be returned by this run. If the Actor is charged per result,
720731
you will not be charged for more results than the given limit.
732+
max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors.
721733
memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified
722734
in the task settings.
723735
run_timeout: Optional timeout for the run. By default, the run uses timeout specified
@@ -746,6 +758,7 @@ async def call(
746758
task_input=task_input,
747759
build=build,
748760
max_items=max_items,
761+
max_total_charge_usd=max_total_charge_usd,
749762
memory_mbytes=memory_mbytes,
750763
run_timeout=run_timeout,
751764
restart_on_error=restart_on_error,
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from __future__ import annotations
2+
3+
import inspect
4+
from decimal import Decimal
5+
from typing import TYPE_CHECKING, Any
6+
7+
import pytest
8+
9+
from apify_client import ApifyClient, ApifyClientAsync
10+
11+
if TYPE_CHECKING:
12+
from collections.abc import Callable
13+
14+
from pytest_httpserver import HTTPServer
15+
16+
pytestmark = pytest.mark.usefixtures('http_client_classes')
17+
18+
RUN = {
19+
'id': 'run-id',
20+
'actId': 'actor-id',
21+
'userId': 'user-id',
22+
'startedAt': '2019-11-30T07:34:24.202Z',
23+
'status': 'SUCCEEDED',
24+
'meta': {'origin': 'API'},
25+
'stats': {'restartCount': 0, 'resurrectCount': 0, 'computeUnits': 0.1},
26+
'options': {'build': 'latest', 'timeoutSecs': 300, 'memoryMbytes': 1024, 'diskMbytes': 2048},
27+
'buildId': 'build-id',
28+
'generalAccess': 'RESTRICTED',
29+
'defaultKeyValueStoreId': 'kvs-id',
30+
'defaultDatasetId': 'dataset-id',
31+
'defaultRequestQueueId': 'rq-id',
32+
'buildNumber': '0.0.1',
33+
'containerUrl': 'https://test.runs.apify.net',
34+
}
35+
36+
STARTERS = [
37+
pytest.param(lambda c, **kw: c.task('task-id').start(**kw), id='start'),
38+
pytest.param(lambda c, **kw: c.task('task-id').call(**kw), id='call'),
39+
]
40+
41+
42+
@pytest.fixture(params=[pytest.param(ApifyClient, id='sync'), pytest.param(ApifyClientAsync, id='async')])
43+
def client(request: pytest.FixtureRequest, httpserver: HTTPServer) -> ApifyClient | ApifyClientAsync:
44+
return request.param(token='test', api_url=httpserver.url_for('/').removesuffix('/'))
45+
46+
47+
async def run(starter: Callable[..., Any], client: ApifyClient | ApifyClientAsync, **kwargs: Any) -> Any:
48+
result = starter(client, **kwargs)
49+
return await result if inspect.isawaitable(result) else result
50+
51+
52+
@pytest.mark.parametrize('starter', STARTERS)
53+
@pytest.mark.parametrize(
54+
('max_total_charge_usd', 'expected_query'),
55+
[
56+
pytest.param(Decimal('1.5'), {'maxTotalChargeUsd': '1.5'}, id='set'),
57+
pytest.param(None, {}, id='omitted'),
58+
],
59+
)
60+
async def test_task_run_sends_max_total_charge_usd(
61+
httpserver: HTTPServer,
62+
client: ApifyClient | ApifyClientAsync,
63+
starter: Callable[..., Any],
64+
max_total_charge_usd: Decimal | None,
65+
expected_query: dict[str, str],
66+
) -> None:
67+
"""`max_total_charge_usd` reaches the task run API as the `maxTotalChargeUsd` query parameter."""
68+
httpserver.expect_request(
69+
'/v2/actor-tasks/task-id/runs', method='POST', query_string=expected_query
70+
).respond_with_json({'data': RUN}, status=201)
71+
httpserver.expect_request('/v2/actor-runs/run-id').respond_with_json({'data': RUN})
72+
73+
await run(starter, client, max_total_charge_usd=max_total_charge_usd)
74+
75+
httpserver.check_assertions()

0 commit comments

Comments
 (0)