Skip to content

Commit e3d36b9

Browse files
authored
feat: add max_total_charge_usd to Actor.start_task and Actor.call_task (#1167)
Moves the SDK to apify-client 3.3.0 and picks up its new task option. - `Actor.start_task` and `Actor.call_task` take `max_total_charge_usd`, the cost cap `Actor.start` and `Actor.call` already have. apify/apify-client-python#1097 added it to the task client in 3.3.0. - `pyproject.toml`: lower bound `>=3.1.0` -> `>=3.3.0`. - apify/apify-client-python#1098 renamed models the SDK imports, so the SDK and its tests use the new names: - `HeadRequest` -> `RequestQueueHeadItem` - `LockedHeadRequest` -> `LockedRequestQueueHeadItem` - `Request` -> `RequestResource` - `RequestDraft` -> `UnprocessedRequest` *✍️ Drafted by Claude Code*
1 parent 889c657 commit e3d36b9

7 files changed

Lines changed: 63 additions & 25 deletions

File tree

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ keywords = [
3535
"scraping",
3636
]
3737
dependencies = [
38-
"apify-client[brotli]>=3.1.0,<4.0.0",
38+
"apify-client[brotli]>=3.3.0,<4.0.0",
3939
"crawlee>=1.8.0,<2.0.0,!=1.10.1",
4040
"cachetools>=5.5.0",
4141
"cryptography>=42.0.0",

‎src/apify/_actor.py‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -958,7 +958,8 @@ async def start(
958958
the run uses the build specified in the default run configuration for the Actor (typically latest).
959959
max_items: Maximum number of dataset items you are charged for, for pay-per-result Actors. It caps the
960960
charge, not the output, so the run can return fewer or more items than this.
961-
max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors.
961+
max_total_charge_usd: A limit on the total charged amount, in USD. Once the run exceeds it, the platform
962+
aborts the run, which takes a few seconds, so the final charge can slightly overshoot the limit.
962963
restart_on_error: If true, the Actor run process will be restarted whenever it exits with
963964
a non-zero status code.
964965
memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified
@@ -1058,8 +1059,10 @@ async def resurrect(
10581059
timeout.
10591060
max_items: Maximum number of items that the resurrected pay-per-result run will return. By default, the
10601061
resurrected run uses the same limit as before. The limit can only be increased.
1061-
max_total_charge_usd: Maximum cost for the resurrected pay-per-event run in USD. By default, the resurrected
1062-
run uses the same limit as before. The limit can only be increased.
1062+
max_total_charge_usd: A limit on the total charged amount of the resurrected run, in USD. Once the run
1063+
exceeds it, the platform aborts the run, which takes a few seconds, so the final charge can slightly
1064+
overshoot the limit. By default, the resurrected run uses the same limit as before. The limit can only
1065+
be increased.
10631066
restart_on_error: If true, the resurrected run process will be restarted whenever it exits with a non-zero
10641067
status code. By default, the resurrected run uses the same setting as before.
10651068
@@ -1109,7 +1112,8 @@ async def call(
11091112
the run uses the build specified in the default run configuration for the Actor (typically latest).
11101113
max_items: Maximum number of dataset items you are charged for, for pay-per-result Actors. It caps the
11111114
charge, not the output, so the run can return fewer or more items than this.
1112-
max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors.
1115+
max_total_charge_usd: A limit on the total charged amount, in USD. Once the run exceeds it, the platform
1116+
aborts the run, which takes a few seconds, so the final charge can slightly overshoot the limit.
11131117
restart_on_error: If true, the Actor run process will be restarted whenever it exits with
11141118
a non-zero status code.
11151119
memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified
@@ -1161,6 +1165,7 @@ async def start_task(
11611165
*,
11621166
build: str | None = None,
11631167
max_items: int | None = None,
1168+
max_total_charge_usd: Decimal | None = None,
11641169
restart_on_error: bool | None = None,
11651170
memory_mbytes: int | None = None,
11661171
timeout: timedelta | Literal['inherit'] | None = None,
@@ -1183,6 +1188,8 @@ async def start_task(
11831188
the run uses the build specified in the default run configuration for the Actor (typically latest).
11841189
max_items: Maximum number of dataset items you are charged for, for pay-per-result Actors. It caps the
11851190
charge, not the output, so the run can return fewer or more items than this.
1191+
max_total_charge_usd: A limit on the total charged amount, in USD. Once the run exceeds it, the platform
1192+
aborts the run, which takes a few seconds, so the final charge can slightly overshoot the limit.
11861193
restart_on_error: If true, the Task run process will be restarted whenever it exits with
11871194
a non-zero status code.
11881195
memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified
@@ -1203,6 +1210,7 @@ async def start_task(
12031210
task_input=task_input,
12041211
build=build,
12051212
max_items=max_items,
1213+
max_total_charge_usd=max_total_charge_usd,
12061214
restart_on_error=restart_on_error,
12071215
memory_mbytes=memory_mbytes,
12081216
run_timeout=self._resolve_run_timeout(timeout),
@@ -1217,6 +1225,7 @@ async def call_task(
12171225
*,
12181226
build: str | None = None,
12191227
max_items: int | None = None,
1228+
max_total_charge_usd: Decimal | None = None,
12201229
restart_on_error: bool | None = None,
12211230
memory_mbytes: int | None = None,
12221231
timeout: timedelta | Literal['inherit'] | None = None,
@@ -1239,6 +1248,8 @@ async def call_task(
12391248
the run uses the build specified in the default run configuration for the Actor (typically latest).
12401249
max_items: Maximum number of dataset items you are charged for, for pay-per-result Actors. It caps the
12411250
charge, not the output, so the run can return fewer or more items than this.
1251+
max_total_charge_usd: A limit on the total charged amount, in USD. Once the run exceeds it, the platform
1252+
aborts the run, which takes a few seconds, so the final charge can slightly overshoot the limit.
12421253
restart_on_error: If true, the Task run process will be restarted whenever it exits with
12431254
a non-zero status code.
12441255
memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified
@@ -1261,6 +1272,7 @@ async def call_task(
12611272
task_input=task_input,
12621273
build=build,
12631274
max_items=max_items,
1275+
max_total_charge_usd=max_total_charge_usd,
12641276
restart_on_error=restart_on_error,
12651277
memory_mbytes=memory_mbytes,
12661278
run_timeout=self._resolve_run_timeout(timeout),

‎src/apify/storage_clients/_apify/_utils.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
if TYPE_CHECKING:
1515
from collections.abc import Iterable
1616

17-
from apify_client._models import HeadRequest, LockedHeadRequest
18-
from apify_client._models import Request as ClientRequest
17+
from apify_client._models import LockedRequestQueueHeadItem, RequestQueueHeadItem
18+
from apify_client._models import RequestResource as ClientRequest
1919
from crawlee.storage_clients.models import AddRequestsResponse
2020

2121
from apify import Configuration
@@ -51,7 +51,7 @@ def hash_api_public_base_url_and_token(configuration: Configuration) -> str:
5151
return compute_short_hash(f'{configuration.api_public_base_url}{configuration.token}'.encode())
5252

5353

54-
def to_crawlee_request(client_request: ClientRequest | HeadRequest | LockedHeadRequest) -> Request:
54+
def to_crawlee_request(client_request: ClientRequest | RequestQueueHeadItem | LockedRequestQueueHeadItem) -> Request:
5555
"""Convert an Apify API client's `Request` model to a Crawlee's `Request` model.
5656
5757
Args:

‎tests/integration/test_request_queue.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import pytest
1111

12-
from apify_client._models import BatchAddResult, RequestDraft
12+
from apify_client._models import BatchAddResult, UnprocessedRequest
1313
from crawlee import service_locator
1414
from crawlee.crawlers import BasicCrawler
1515

@@ -1290,7 +1290,7 @@ async def test_request_queue_deduplication_unprocessed_requests(
12901290
def return_unprocessed_requests(requests: list[dict], *_: Any, **__: Any) -> BatchAddResult:
12911291
"""Simulate API returning unprocessed requests."""
12921292
unprocessed_requests = [
1293-
RequestDraft.model_construct(
1293+
UnprocessedRequest.model_construct(
12941294
url=request['url'],
12951295
unique_key=request['uniqueKey'],
12961296
method=request['method'],

‎tests/unit/actor/test_actor_helpers.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,32 @@ async def test_max_items_forwarded_to_client(
158158
assert apify_client_async_patcher.calls[client_resource][client_method][0][1]['max_items'] == 42
159159

160160

161+
@pytest.mark.parametrize(
162+
('client_resource', 'client_method', 'sdk_method'),
163+
[
164+
pytest.param('actor', 'start', 'start', id='start'),
165+
pytest.param('actor', 'call', 'call', id='call'),
166+
pytest.param('task', 'start', 'start_task', id='start_task'),
167+
pytest.param('task', 'call', 'call_task', id='call_task'),
168+
],
169+
)
170+
async def test_max_total_charge_usd_forwarded_to_client(
171+
apify_client_async_patcher: ApifyClientAsyncPatcher,
172+
fake_actor_run: Run,
173+
client_resource: str,
174+
client_method: str,
175+
sdk_method: str,
176+
) -> None:
177+
"""`max_total_charge_usd` passed to any of the run-starting helpers reaches the API client."""
178+
apify_client_async_patcher.patch(client_resource, client_method, return_value=fake_actor_run)
179+
180+
async with Actor:
181+
await getattr(Actor, sdk_method)('some-id', max_total_charge_usd=Decimal('2.5'))
182+
183+
kwargs = apify_client_async_patcher.calls[client_resource][client_method][0][1]
184+
assert kwargs['max_total_charge_usd'] == Decimal('2.5')
185+
186+
161187
async def test_abort_actor_run(apify_client_async_patcher: ApifyClientAsyncPatcher, fake_actor_run: Run) -> None:
162188
apify_client_async_patcher.patch('run', 'abort', return_value=fake_actor_run)
163189
run_id = 'some-run-id'

‎tests/unit/storage_clients/test_apify_request_queue_client.py‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@
1212
from apify_client._models import (
1313
AddedRequest,
1414
BatchAddResult,
15-
HeadRequest,
16-
LockedHeadRequest,
1715
LockedRequestQueueHead,
18-
RequestDraft,
16+
LockedRequestQueueHeadItem,
1917
RequestLockInfo,
2018
RequestQueueHead,
19+
RequestQueueHeadItem,
2120
RequestQueueStats,
2221
RequestRegistration,
22+
UnprocessedRequest,
2323
)
24-
from apify_client._models import Request as ClientRequest
24+
from apify_client._models import RequestResource as ClientRequest
2525
from crawlee.storage_clients.models import AddRequestsResponse, ProcessedRequest, RequestQueueMetadata
2626

2727
from apify import Request
@@ -66,7 +66,7 @@ def _batch_result(
6666
for request in processed
6767
],
6868
unprocessed_requests=[
69-
RequestDraft.model_construct(
69+
UnprocessedRequest.model_construct(
7070
unique_key=request.unique_key,
7171
url=request.url,
7272
method=request.method,
@@ -589,9 +589,9 @@ async def test_shared_is_finished_does_not_refetch_requests_confirmed_in_an_unfi
589589
api_client.get_request.assert_awaited_once_with(straggler_id)
590590

591591

592-
def _locked_item(request: Request, *, lock_expires_at: datetime) -> LockedHeadRequest:
592+
def _locked_item(request: Request, *, lock_expires_at: datetime) -> LockedRequestQueueHeadItem:
593593
"""Build a single locked head entry for `request` with the given lock expiry."""
594-
return LockedHeadRequest(
594+
return LockedRequestQueueHeadItem(
595595
id=unique_key_to_request_id(request.unique_key),
596596
unique_key=request.unique_key,
597597
url=request.url,
@@ -602,7 +602,7 @@ def _locked_item(request: Request, *, lock_expires_at: datetime) -> LockedHeadRe
602602

603603

604604
def _locked_head(
605-
items: Sequence[LockedHeadRequest],
605+
items: Sequence[LockedRequestQueueHeadItem],
606606
*,
607607
queue_has_locked_requests: bool = True,
608608
) -> LockedRequestQueueHead:
@@ -850,9 +850,9 @@ async def test_reclaim_request_frees_in_progress() -> None:
850850
assert second.unique_key == request.unique_key
851851

852852

853-
def _head_item(request: Request) -> HeadRequest:
853+
def _head_item(request: Request) -> RequestQueueHeadItem:
854854
"""Build a `list_head` item for the given request."""
855-
return HeadRequest(
855+
return RequestQueueHeadItem(
856856
id=unique_key_to_request_id(request.unique_key),
857857
unique_key=request.unique_key,
858858
url=request.url,
@@ -861,7 +861,7 @@ def _head_item(request: Request) -> HeadRequest:
861861
)
862862

863863

864-
def _head(*items: HeadRequest) -> RequestQueueHead:
864+
def _head(*items: RequestQueueHeadItem) -> RequestQueueHead:
865865
"""Build a `list_head` response wrapping the given items."""
866866
return RequestQueueHead(
867867
limit=200,

‎uv.lock‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)